From d8cdbb30bb29f94b510ca179e2ddefe798aa8030 Mon Sep 17 00:00:00 2001 From: Ashwathi Shiva <56001041+ashwathishiva@users.noreply.github.com> Date: Wed, 24 Jun 2020 10:28:22 -0400 Subject: [PATCH] Preflight openssl verify (#436) * qliksense preflight verify-ca-chain, included into all preflight checks and doc updates --- cmd/qliksense/preflight.go | 39 ++++++++++++ cmd/qliksense/root.go | 1 + docs/preflight_checks.md | 18 ++++++ pkg/preflight/all_checks.go | 10 +++ pkg/preflight/verify_ca.go | 118 ++++++++++++++++++++++++++++++++++++ 5 files changed, 186 insertions(+) create mode 100644 pkg/preflight/verify_ca.go diff --git a/cmd/qliksense/preflight.go b/cmd/qliksense/preflight.go index e711fb6..266929c 100644 --- a/cmd/qliksense/preflight.go +++ b/cmd/qliksense/preflight.go @@ -472,3 +472,42 @@ func pfCleanupCmd(q *qliksense.Qliksense) *cobra.Command { f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode") return pfCleanCmd } + +func pfVerifyCAChainCmd(q *qliksense.Qliksense) *cobra.Command { + out := ansi.NewColorableStdout() + preflightOpts := &preflight.PreflightOptions{ + MongoOptions: &preflight.MongoOptions{}, + } + + var pfVerifyCAChainCmd = &cobra.Command{ + Use: "verify-ca-chain", + Short: "verify-ca-chain using openssl verify", + Long: `verify the CA chain using openssl verify to ensure that mongodb certificate is valid`, + Example: `qliksense preflight verify-ca-chain`, + RunE: func(cmd *cobra.Command, args []string) error { + qp := &preflight.QliksensePreflight{Q: q, P: preflightOpts, CG: &api.ClientGoUtils{Verbose: preflightOpts.Verbose}} + + // Preflight service check + namespace, kubeConfigContents, err := qp.CG.LoadKubeConfigAndNamespace() + if err != nil { + fmt.Fprintf(out, "%s\n", Red("FAILED")) + fmt.Printf("Error: %v\n", err) + return nil + } + + if namespace == "" { + namespace = "default" + } + if err = qp.VerifyCAChain(kubeConfigContents, namespace, preflightOpts, false); err != nil { + fmt.Fprintf(out, "%s\n", Red("FAILED")) + fmt.Printf("Error: %v\n", err) + return nil + } + fmt.Fprintf(out, "%s\n", Green("PASSED")) + return nil + }, + } + f := pfVerifyCAChainCmd.Flags() + f.BoolVarP(&preflightOpts.Verbose, "verbose", "v", false, "verbose mode") + return pfVerifyCAChainCmd +} diff --git a/cmd/qliksense/root.go b/cmd/qliksense/root.go index 6495128..e6dbf5f 100644 --- a/cmd/qliksense/root.go +++ b/cmd/qliksense/root.go @@ -209,6 +209,7 @@ func rootCmd(p *qliksense.Qliksense) *cobra.Command { preflightCmd.AddCommand(pfCreateRoleBindingCheckCmd(p)) preflightCmd.AddCommand(pfCreateServiceAccountCheckCmd(p)) preflightCmd.AddCommand(pfCreateAuthCheckCmd(p)) + preflightCmd.AddCommand(pfVerifyCAChainCmd(p)) preflightCmd.AddCommand(pfCleanupCmd(p)) cmd.AddCommand(preflightCmd) diff --git a/docs/preflight_checks.md b/docs/preflight_checks.md index 8e572df..8b59753 100644 --- a/docs/preflight_checks.md +++ b/docs/preflight_checks.md @@ -305,3 +305,21 @@ Removing mongo check components... Preflight cleanup complete ``` + +### Verify-ca-chain check +We use the command below to verify the ca certificate chain and server certificate. We run this check over mongodbUrl and discoveryUrl we inferred from idpconfigs in the CR. +```shell +$ qliksense preflight preflight verify-ca-chain -v + +Preflight verify-ca-chain check... +----------------------------------- +Openssl verify mongodbUrl: +Mongodb url inferred form CR: +Host: + +Openssl verify discoveryUrl: +Discovery url: +Host: +Completed preflight verify-CA-chain check +PASSED +``` \ No newline at end of file diff --git a/pkg/preflight/all_checks.go b/pkg/preflight/all_checks.go index 94d9a5b..455bf51 100644 --- a/pkg/preflight/all_checks.go +++ b/pkg/preflight/all_checks.go @@ -103,6 +103,16 @@ func (qp *QliksensePreflight) RunAllPreflightChecks(kubeConfigContents []byte, n } totalCount++ + // Preflight verify ca chain check + if err := qp.VerifyCAChain(kubeConfigContents, namespace, preflightOpts, false); err != nil { + fmt.Fprintf(out, "%s\n", Red("FAILED")) + fmt.Printf("Error: %v\n\n", err) + } else { + fmt.Fprintf(out, "%s\n\n", Green("PASSED")) + checkCount++ + } + totalCount++ + if checkCount == totalCount { // All preflight checks were successful return nil diff --git a/pkg/preflight/verify_ca.go b/pkg/preflight/verify_ca.go new file mode 100644 index 0000000..b90df96 --- /dev/null +++ b/pkg/preflight/verify_ca.go @@ -0,0 +1,118 @@ +package preflight + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "net/url" + "strings" + + qapi "github.com/qlik-oss/sense-installer/pkg/api" +) + +func (qp *QliksensePreflight) VerifyCAChain(kubeConfigContents []byte, namespace string, preflightOpts *PreflightOptions, cleanup bool) error { + + var currentCR *qapi.QliksenseCR + var err error + qConfig := qapi.NewQConfig(qp.Q.QliksenseHome) + qConfig.SetNamespace(namespace) + + fmt.Print("Preflight verify-ca-chain check... ") + qp.CG.LogVerboseMessage("\n----------------------------------- \n") + + currentCR, err = qConfig.GetCurrentCR() + if err != nil { + qp.CG.LogVerboseMessage("Unable to retrieve current CR: %v\n", err) + return err + } + decryptedCR, err := qConfig.GetDecryptedCr(currentCR) + if err != nil { + qp.CG.LogVerboseMessage("An error occurred while retrieving mongodbUrl from current CR: %v\n", err) + return err + } + + // infer ca certs form CR + caCertificates := strings.TrimSpace(decryptedCR.Spec.GetFromSecrets("qliksense", "caCertificates")) + + fmt.Println("Openssl verify mongodbUrl:") + // infer mongodb url from CR + mongodbUrl := strings.TrimSpace(decryptedCR.Spec.GetFromSecrets("qliksense", "mongodbUri")) + qp.CG.LogVerboseMessage("Mongodb url inferred form CR: %s\n", mongodbUrl) + + // parse out server and port from mongodb url and execute openssl verify + if err := qp.extractCertAndVerify(mongodbUrl, caCertificates); err != nil { + return err + } + + fmt.Printf("\nOpenssl verify discoveryUrl:\n") + // infer idpConfigs form CR + idpConfigs := strings.TrimSpace(decryptedCR.Spec.GetFromSecrets("identity-providers", "idpConfigs")) + + data := []map[string]interface{}{} + if err := json.Unmarshal([]byte(idpConfigs), &data); err != nil { + panic(err) + } + + var discoveryUrl string + for _, idpData := range data { + discoveryUrl = idpData["discoveryUrl"].(string) + qp.CG.LogVerboseMessage("Discovery url: %s\n", discoveryUrl) + } + if err := qp.extractCertAndVerify(discoveryUrl, caCertificates); err != nil { + return err + } + + qp.CG.LogVerboseMessage("Completed preflight verify-ca-chain check\n") + return nil +} + +func (qp *QliksensePreflight) extractCertAndVerify(server string, caCertificates string) error { + u, err := url.Parse(server) + if err != nil { + return fmt.Errorf("unable to parse url: %v", err) + } + + switch strings.ToLower(u.Scheme) { + case "http": + return fmt.Errorf("http url is not supported for this operation") + case "https": + if u.Port() == "" { + u.Host += ":443" + } + } + + qp.CG.LogVerboseMessage("Host: %s, port: %s\n", u.Host, u.Port()) + conn, err := tls.Dial("tcp", u.Host, &tls.Config{}) + qp.CG.LogVerboseMessage("Host: %s\n", u.Host) + if err != nil { + return fmt.Errorf("failed to connect: " + err.Error()) + } + defer conn.Close() + + // Get the ConnectionState struct as that's the one which gives us x509.Certificate struct + x509Certificates := conn.ConnectionState().PeerCertificates + + if len(x509Certificates) == 0 { + return fmt.Errorf("no server certificates retrieved from the server") + } + if len(x509Certificates) > 1 { + return fmt.Errorf("more than 1 server certificate retrieved from the server") + } + // execute verify cmd + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM([]byte(caCertificates)) + if !ok { + return fmt.Errorf("failed to parse root certificate.") + } + + opts := x509.VerifyOptions{ + Roots: roots, + DNSName: u.Hostname(), + Intermediates: x509.NewCertPool(), + } + if _, err := x509Certificates[0].Verify(opts); err != nil { + return fmt.Errorf("failed to verify certificate: " + err.Error()) + } + return nil +}