package cmd import ( "context" "crypto/tls" "crypto/x509" "fmt" "os" "strings" "github.com/grantae/certinfo" pkgcmd "github.com/linkerd/linkerd2/pkg/cmd" "github.com/linkerd/linkerd2/pkg/k8s" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ) var emitLog bool type certificate struct { pod string container string Certificate []*x509.Certificate err error } type identityOptions struct { pod string namespace string selector string } func newIdentityOptions() *identityOptions { return &identityOptions{ pod: "", selector: "", } } func newCmdIdentity() *cobra.Command { emitLog = false options := newIdentityOptions() cmd := &cobra.Command{ Use: "identity [flags] (PODS)", Short: "Display the certificate(s) of one or more selected pod(s)", Long: `Display the certificate(s) of one or more selected pod(s). This command initiates a port-forward to a given pod or a set of pods and fetches the TLS certificate. `, Example: ` # Get certificate from pod foo-bar in the default namespace. linkerd identity foo-bar # Get certificate from all pods with the label name=nginx linkerd identity -l name=nginx `, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0) if err != nil { return nil, cobra.ShellCompDirectiveError } if options.namespace == "" { options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext) } cc := k8s.NewCommandCompletion(k8sAPI, options.namespace) // insert pod resource type as first argument to suggest // pod resources only args = append([]string{k8s.Pod}, args...) results, err := cc.Complete(args, toComplete) if err != nil { return nil, cobra.ShellCompDirectiveError } return results, cobra.ShellCompDirectiveDefault }, RunE: func(cmd *cobra.Command, args []string) error { if options.namespace == "" { options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext) } k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0) if err != nil { return err } if len(args) == 0 && options.selector == "" { return fmt.Errorf("Provide the pod name argument or use the selector flag") } pods, err := getPods(cmd.Context(), k8sAPI, options.namespace, options.selector, args) if err != nil { return err } resultCerts := getCertificate(k8sAPI, pods, k8s.ProxyAdminPortName, emitLog) if len(resultCerts) == 0 { fmt.Print("Could not fetch Certificate. Ensure that the pod(s) are meshed by running `linkerd inject`\n") return nil } for i, resultCert := range resultCerts { fmt.Printf("\nPOD %s (%d of %d)\n\n", resultCert.pod, i+1, len(resultCerts)) if resultCert.err != nil { fmt.Printf("\n%s\n", resultCert.err) return nil } for _, cert := range resultCert.Certificate { if cert.IsCA { continue } result, err := certinfo.CertificateText(cert) if err != nil { fmt.Printf("\n%s\n", err) return nil } fmt.Print(result) } } return nil }, } cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the pod") cmd.PersistentFlags().StringVarP(&options.selector, "selector", "l", options.selector, "Selector (label query) to filter on, supports ‘=’, ‘==’, and ‘!=’ ") pkgcmd.ConfigureNamespaceFlagCompletion(cmd, []string{"namespace"}, kubeconfigPath, impersonate, impersonateGroup, kubeContext) return cmd } func getCertificate(k8sAPI *k8s.KubernetesAPI, pods []corev1.Pod, portName string, emitLog bool) []certificate { var certificates []certificate for _, pod := range pods { container, err := getContainerWithPort(pod, portName) if err != nil { certificates = append(certificates, certificate{ pod: pod.GetName(), err: err, }) return certificates } cert, err := getContainerCertificate(k8sAPI, pod, container, portName, emitLog) certificates = append(certificates, certificate{ pod: pod.GetName(), container: container.Name, Certificate: cert, err: err, }) } return certificates } func getContainerWithPort(pod corev1.Pod, portName string) (corev1.Container, error) { var container corev1.Container if pod.Status.Phase != corev1.PodRunning { return container, fmt.Errorf("pod not running: %s", pod.GetName()) } containers := append(pod.Spec.InitContainers, pod.Spec.Containers...) for _, c := range containers { if c.Name != k8s.ProxyContainerName { continue } for _, p := range c.Ports { if p.Name == portName { return c, nil } } } return container, fmt.Errorf("failed to find %s port in %s container for given pod spec", portName, k8s.ProxyContainerName) } func getContainerCertificate(k8sAPI *k8s.KubernetesAPI, pod corev1.Pod, container corev1.Container, portName string, emitLog bool) ([]*x509.Certificate, error) { portForward, err := k8s.NewContainerMetricsForward(k8sAPI, pod, container, emitLog, portName) if err != nil { return nil, err } defer portForward.Stop() if err = portForward.Init(); err != nil { fmt.Fprintf(os.Stderr, "Error running port-forward: %s", err) return nil, err } certURL := portForward.URLFor("") return getCertResponse(certURL, pod) } func getCertResponse(url string, pod corev1.Pod) ([]*x509.Certificate, error) { serverName, err := k8s.PodIdentity(&pod) if err != nil { return nil, err } connURL := strings.TrimPrefix(url, "http://") conn, err := tls.Dial("tcp", connURL, &tls.Config{ // We want to connect directly to a proxy port to dump its certificate. We don't necessarily // want to verify the server's certificate, since this is purely for diagnostics and may be // used when a proxy's issuer doesn't match the control plane's trust root. //nolint:gosec InsecureSkipVerify: true, ServerName: serverName, }) if err != nil { return nil, err } cert := conn.ConnectionState().PeerCertificates return cert, nil } func getPods(ctx context.Context, clientset kubernetes.Interface, namespace string, selector string, args []string) ([]corev1.Pod, error) { if len(args) > 0 { var pods []corev1.Pod for _, arg := range args { pod, err := clientset.CoreV1().Pods(namespace).Get(ctx, arg, metav1.GetOptions{}) if err != nil { return nil, err } pods = append(pods, *pod) } return pods, nil } podList, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ LabelSelector: selector, }) if err != nil { return nil, err } return podList.Items, nil }