From 64d5330ae53e5e36ddc82ca4ece30773362f3721 Mon Sep 17 00:00:00 2001 From: David Rohnow Date: Tue, 11 Apr 2023 19:34:02 -0700 Subject: [PATCH] certificate collector (#1112) * adding pkg/collect/certificates.go and pkg/collect/certificates_test.go files * adding collector.go and collector_shared.go files * ran make schemas * update zz_generated.deepcopy.go Co-authored-by: Dexter Yan Co-authored-by: Chris Sanders Co-authored-by: Xav Paice --- config/crds/troubleshoot.sh_collectors.yaml | 21 ++ config/crds/troubleshoot.sh_preflights.yaml | 21 ++ .../crds/troubleshoot.sh_supportbundles.yaml | 21 ++ .../troubleshoot/v1beta2/collector_shared.go | 12 + .../v1beta2/zz_generated.deepcopy.go | 51 ++++ pkg/collect/certificates.go | 254 ++++++++++++++++++ pkg/collect/certificates_test.go | 222 +++++++++++++++ pkg/collect/collector.go | 5 + schemas/collector-troubleshoot-v1beta2.json | 32 +++ schemas/preflight-troubleshoot-v1beta2.json | 32 +++ .../supportbundle-troubleshoot-v1beta2.json | 32 +++ 11 files changed, 703 insertions(+) create mode 100644 pkg/collect/certificates.go create mode 100644 pkg/collect/certificates_test.go diff --git a/config/crds/troubleshoot.sh_collectors.yaml b/config/crds/troubleshoot.sh_collectors.yaml index 56426279..7cf96242 100644 --- a/config/crds/troubleshoot.sh_collectors.yaml +++ b/config/crds/troubleshoot.sh_collectors.yaml @@ -82,6 +82,27 @@ spec: required: - namespace type: object + certificates: + properties: + collectorName: + type: string + configMaps: + additionalProperties: + items: + type: string + type: array + type: object + exclude: + type: BoolString + name: + type: string + secrets: + additionalProperties: + items: + type: string + type: array + type: object + type: object clusterInfo: properties: collectorName: diff --git a/config/crds/troubleshoot.sh_preflights.yaml b/config/crds/troubleshoot.sh_preflights.yaml index 9c4bc949..7e697839 100644 --- a/config/crds/troubleshoot.sh_preflights.yaml +++ b/config/crds/troubleshoot.sh_preflights.yaml @@ -1526,6 +1526,27 @@ spec: required: - namespace type: object + certificates: + properties: + collectorName: + type: string + configMaps: + additionalProperties: + items: + type: string + type: array + type: object + exclude: + type: BoolString + name: + type: string + secrets: + additionalProperties: + items: + type: string + type: array + type: object + type: object clusterInfo: properties: collectorName: diff --git a/config/crds/troubleshoot.sh_supportbundles.yaml b/config/crds/troubleshoot.sh_supportbundles.yaml index 43b0a29e..d8a9a2b6 100644 --- a/config/crds/troubleshoot.sh_supportbundles.yaml +++ b/config/crds/troubleshoot.sh_supportbundles.yaml @@ -1557,6 +1557,27 @@ spec: required: - namespace type: object + certificates: + properties: + collectorName: + type: string + configMaps: + additionalProperties: + items: + type: string + type: array + type: object + exclude: + type: BoolString + name: + type: string + secrets: + additionalProperties: + items: + type: string + type: array + type: object + type: object clusterInfo: properties: collectorName: diff --git a/pkg/apis/troubleshoot/v1beta2/collector_shared.go b/pkg/apis/troubleshoot/v1beta2/collector_shared.go index 70f4387b..506c0595 100644 --- a/pkg/apis/troubleshoot/v1beta2/collector_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/collector_shared.go @@ -216,6 +216,13 @@ type RegistryImages struct { ImagePullSecrets *ImagePullSecrets `json:"imagePullSecret,omitempty" yaml:"imagePullSecret,omitempty"` } +type Certificates struct { + CollectorMeta `json:",inline" yaml:",inline"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Secrets map[string][]string `json:"secrets,omitempty" yaml:"secrets,omitempty"` + ConfigMaps map[string][]string `json:"configMaps,omitempty" yaml:"configMaps,omitempty"` +} + type Collect struct { ClusterInfo *ClusterInfo `json:"clusterInfo,omitempty" yaml:"clusterInfo,omitempty"` ClusterResources *ClusterResources `json:"clusterResources,omitempty" yaml:"clusterResources,omitempty"` @@ -238,6 +245,7 @@ type Collect struct { Longhorn *Longhorn `json:"longhorn,omitempty" yaml:"longhorn,omitempty"` RegistryImages *RegistryImages `json:"registryImages,omitempty" yaml:"registryImages,omitempty"` Sysctl *Sysctl `json:"sysctl,omitempty" yaml:"sysctl,omitempty"` + Certificates *Certificates `json:"certificates,omitempty" yaml:"certificates,omitempty"` } func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSubjectAccessReviewSpec { @@ -539,6 +547,10 @@ func (c *Collect) GetName() string { collector = "sysctl" name = c.Sysctl.Name } + if c.Certificates != nil { + collector = "certificates" + name = c.Certificates.CollectorName + } if collector == "" { return "" diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go index 48f4dd5d..7a6068ff 100644 --- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go @@ -559,6 +559,52 @@ func (in *CertificateAnalyze) DeepCopy() *CertificateAnalyze { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Certificates) DeepCopyInto(out *Certificates) { + *out = *in + in.CollectorMeta.DeepCopyInto(&out.CollectorMeta) + if in.Secrets != nil { + in, out := &in.Secrets, &out.Secrets + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + if in.ConfigMaps != nil { + in, out := &in.ConfigMaps, &out.ConfigMaps + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Certificates. +func (in *Certificates) DeepCopy() *Certificates { + if in == nil { + return nil + } + out := new(Certificates) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterInfo) DeepCopyInto(out *ClusterInfo) { *out = *in @@ -790,6 +836,11 @@ func (in *Collect) DeepCopyInto(out *Collect) { *out = new(Sysctl) (*in).DeepCopyInto(*out) } + if in.Certificates != nil { + in, out := &in.Certificates, &out.Certificates + *out = new(Certificates) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Collect. diff --git a/pkg/collect/certificates.go b/pkg/collect/certificates.go new file mode 100644 index 00000000..86776bba --- /dev/null +++ b/pkg/collect/certificates.go @@ -0,0 +1,254 @@ +package collect + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "encoding/pem" + "time" + + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +type CollectCertificates struct { + Collector *troubleshootv1beta2.Certificates + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + Context context.Context + RBACErrors +} + +// Certificate collection struct +type CertCollection struct { + Source *CertificateSource `json:"source"` + Errors []string `json:"errors"` + CertificateChain []ParsedCertificate `json:"certificateChain"` +} + +// Certificate source +type CertificateSource struct { + SecretName string `json:"secret,omitempty"` + ConfigMapName string `json:"configMap,omitempty"` + Namespace string `json:"namespace,omitempty"` +} + +// Certificate Struct +type ParsedCertificate struct { + CertName string `json:"certificate"` + Subject string `json:"subject"` + SubjectAlternativeNames []string `json:"subjectAlternativeNames"` + Issuer string `json:"issuer"` + NotAfter time.Time `json:"notAfter"` + NotBefore time.Time `json:"notBefore"` + IsValid bool `json:"isValid"` + IsCA bool `json:"isCA"` +} + +func (c *CollectCertificates) Title() string { + return getCollectorName(c) +} + +func (c *CollectCertificates) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectCertificates) Collect(progressChan chan<- interface{}) (CollectorResult, error) { + + output := NewResult() + results := []CertCollection{} + + // collect certificates from secrets + for secretName, namespaces := range c.Collector.Secrets { + for _, namespace := range namespaces { + + secretCollections := secretCertCollector(secretName, namespace, c.Client) + results = append(results, secretCollections...) + } + } + + // collect certificates from configMaps + for configMapName, namespaces := range c.Collector.ConfigMaps { + for _, namespace := range namespaces { + configMapCollections := configMapCertCollector(configMapName, namespace, c.Client) + results = append(results, configMapCollections...) + } + } + + certsJson, errCertJson := json.MarshalIndent(results, "", "\t") + if errCertJson != nil { + return nil, errCertJson + } + + filePath := "certificates/certificates.json" + + err := output.SaveResult(c.BundlePath, filePath, bytes.NewBuffer(certsJson)) + if err != nil { + return nil, err + } + + return output, nil +} + +// configmap certificate collector +func configMapCertCollector(configMapName string, namespace string, client kubernetes.Interface) []CertCollection { + + results := []CertCollection{} + trackErrors := []string{} + collection := []ParsedCertificate{} + + getOptions := metav1.GetOptions{} + + // Collect from configMaps + configMap, err := client.CoreV1().ConfigMaps(namespace).Get(context.Background(), configMapName, getOptions) + if err != nil { + + // collect certificate source information + source := &CertificateSource{ + SecretName: configMapName, + Namespace: namespace, + } + trackErrors = append(trackErrors, "Either the configMap does not exist in this namespace or RBAC permissions are preventing certificate collection") + + results = append(results, CertCollection{ + Source: source, + Errors: trackErrors, + CertificateChain: collection, + }) + + return results + + } + + //Collect from configMap + source := &CertificateSource{ + ConfigMapName: configMap.Name, + Namespace: configMap.Namespace, + } + for certName, c := range configMap.Data { + + certs := []byte(c) + + certInfo, _ := CertParser(certName, certs) + + collection = append(collection, certInfo...) + } + results = append(results, CertCollection{ + Source: source, + Errors: trackErrors, + CertificateChain: collection, + }) + + return results +} + +// secret certificate collector +func secretCertCollector(secretName string, namespace string, client kubernetes.Interface) []CertCollection { + + results := []CertCollection{} + trackErrors := []string{} + collection := []ParsedCertificate{} + + getOptions := metav1.GetOptions{} + // Collect from secrets + secret, err := client.CoreV1().Secrets(namespace).Get(context.Background(), secretName, getOptions) + if err != nil { + + // collect certificate source information + source := &CertificateSource{ + SecretName: secretName, + Namespace: namespace, + } + trackErrors = append(trackErrors, "Either the secret does not exist in this namespace or RBAC permissions are prenventing certificate collection") + + results = append(results, CertCollection{ + Source: source, + Errors: trackErrors, + CertificateChain: collection, + }) + + return results + + } + + // Collect from secret + source := &CertificateSource{ + SecretName: secret.Name, + Namespace: secret.Namespace, + } + + for certName, certs := range secret.Data { + certInfo, _ := CertParser(certName, certs) + + collection = append(collection, certInfo...) + + } + results = append(results, CertCollection{ + Source: source, + Errors: trackErrors, + CertificateChain: collection, + }) + + return results +} + +// decode pem and validate data source contains +func decodePem(certInput string) (tls.Certificate, []string) { + var cert tls.Certificate + trackErrors := []string{} + certPEMBlock := []byte(certInput) + var certDERBlock *pem.Block + for { + certDERBlock, certPEMBlock = pem.Decode(certPEMBlock) + if certDERBlock == nil { + trackErrors = append(trackErrors, "decodePem function error: cert block is empty") + + break + } + if certDERBlock.Type == "CERTIFICATE" { + cert.Certificate = append(cert.Certificate, certDERBlock.Bytes) + } + } + return cert, trackErrors +} + +// Certificate parser +func CertParser(certName string, certs []byte) ([]ParsedCertificate, []string) { + //TODO: return trackErrors as well. + currentTime := time.Now() + data := string(certs) + certInfo := []ParsedCertificate{} + trackErrors := []string{} + + certChain, decodePemTrackErrors := decodePem(data) + + trackErrors = append(trackErrors, decodePemTrackErrors...) + + for _, cert := range certChain.Certificate { + + //parsed SSL certificate + parsedCert, errParse := x509.ParseCertificate(cert) + if errParse != nil { + trackErrors = append(trackErrors, errParse.Error()) + continue + } + + certInfo = append(certInfo, ParsedCertificate{ + CertName: certName, + Subject: parsedCert.Subject.ToRDNSequence().String(), + SubjectAlternativeNames: parsedCert.DNSNames, + Issuer: parsedCert.Issuer.ToRDNSequence().String(), + NotAfter: parsedCert.NotAfter, + NotBefore: parsedCert.NotBefore, + IsValid: currentTime.Before(parsedCert.NotAfter), + IsCA: parsedCert.IsCA, + }) + } + return certInfo, trackErrors +} diff --git a/pkg/collect/certificates_test.go b/pkg/collect/certificates_test.go new file mode 100644 index 00000000..ccaa5a04 --- /dev/null +++ b/pkg/collect/certificates_test.go @@ -0,0 +1,222 @@ +package collect + +import ( + "strings" + "testing" +) + +var chain = `-----BEGIN CERTIFICATE----- +MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ +hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa +rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv +zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW +r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V +-----END CERTIFICATE----- +` + +var chain2 = `-----BEGIN CERTIFICATE----- +MIIG5jCCBc6gAwIBAgIQAze5KDR8YKauxa2xIX84YDANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA3MTEwOTEyMDAwMFoXDTIxMTExMDAwMDAwMFowaTEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTEoMCYGA1UEAxMfRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgQ0EtMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPOWYth1bhn/ +PzR8SU8xfg0ETpmB4rOFVZEwscCvcLssqOcYqj9495BoUoYBiJfiOwZlkKq9ZXbC +7L4QWzd4g2B1Rca9dKq2n6Q6AVAXxDlpufFP74LByvNK28yeUE9NQKM6kOeGZrzw +PnYoTNF1gJ5qNRQ1A57bDIzCKK1Qss72kaPDpQpYSfZ1RGy6+c7pqzoC4E3zrOJ6 +4GAiBTyC01Li85xH+DvYskuTVkq/cKs+6WjIHY9YHSpNXic9rQpZL1oRIEDZaARo +LfTAhAsKG3jf7RpY3PtBWm1r8u0c7lwytlzs16YDMqbo3rcoJ1mIgP97rYlY1R4U +pPKwcNSgPqcCAwEAAaOCA4UwggOBMA4GA1UdDwEB/wQEAwIBhjA7BgNVHSUENDAy +BggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUH +AwgwggHEBgNVHSAEggG7MIIBtzCCAbMGCWCGSAGG/WwCATCCAaQwOgYIKwYBBQUH +AgEWLmh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5o +dG0wggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0 +AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1 +AHQAZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABp +AGcAaQBDAGUAcgB0ACAARQBWACAAQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBl +AGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBo +AGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAg +AGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAg +AGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wEgYDVR0TAQH/BAgwBgEB/wIBADCB +gwYIKwYBBQUHAQEEdzB1MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy +dC5jb20wTQYIKwYBBQUHMAKGQWh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NBQ2Vy +dHMvRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3J0MIGPBgNVHR8EgYcw +gYQwQKA+oDyGOmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEhpZ2hB +c3N1cmFuY2VFVlJvb3RDQS5jcmwwQKA+oDyGOmh0dHA6Ly9jcmw0LmRpZ2ljZXJ0 +LmNvbS9EaWdpQ2VydEhpZ2hBc3N1cmFuY2VFVlJvb3RDQS5jcmwwHQYDVR0OBBYE +FExYyyXwQU9S9CjIgUObpqig5pLlMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSYJhoI +Au9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQBMeheHKF0XvLIyc7/NLvVYMR3wsXFU +nNabZ5PbLwM+Fm8eA8lThKNWYB54lBuiqG+jpItSkdfdXJW777UWSemlQk808kf/ +roF/E1S3IMRwFcuBCoHLdFfcnN8kpCkMGPAc5K4HM+zxST5Vz25PDVR708noFUjU +xbvcNRx3RQdIRYW9135TuMAW2ZXNi419yWBP0aKb49Aw1rRzNubS+QOy46T15bg+ +BEkAui6mSnKDcp33C4ypieez12Qf1uNgywPE3IjpnSUBAHHLA7QpYCWP+UbRe3Gu +zVMSW4SOwg/H7ZMZ2cn6j1g0djIvruFQFGHUqFijyDATI+/GJYw2jxyA +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE-----` + +var chain3 = `-----BEGIN CERTIFICATE----- +MIIDejCCAmKgAwIBAgIEAZaq0DANBgkqhkiG9w0BAQsFADAuMRgwFgYDVQQDEw9Q +cm9qZWN0IENvbnRvdXIxEjAQBgNVBAUTCTYxNTkyOTg5MTAeFw0yMzAyMjQwNDI3 +MThaFw0yNDAyMjUwNDI3MTZaMBAxDjAMBgNVBAMTBWVudm95MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqsNmmxb1ICso6Ay25lapcRyvLxAX/5u422uV +eiNn5jseCVXfg1jJr2Symrgou2dgMtIpZoVKT7w0el8sNpD+az5oMOWUNfTGEpYI +5zNAhtiedxCWcX15gfezOZ/DECL7HP8U37JFVdazm0CjvlQWI+8rFGvFQJJFDLYJ +h0fHGfbX6L/ST6cANtkXZyIU6CYFSgniuuDjHmQnQr6CC8lkisJxY5QVS7MZ02RR +nU/dK14ABY+mo/0ZeBKR5si04hr4i18nJJnk4DnHN+jQ/WWWSO1yLqr9kAOj37dA +nRAeKuzkx/VbU8DC/3sh0otcazoWO470D+irOy1is0whDArLNwIDAQABo4G9MIG6 +MA4GA1UdDwEB/wQEAwIE8DAdBgNVHQ4EFgQUbAavOY+vIXgc44k8GvLHH6mdzzYw +HwYDVR0jBBgwFoAUb+S3Mu7cbwZiqZaEHTEMyUhLyH4waAYDVR0RBGEwX4IFZW52 +b3mCFGVudm95LnByb2plY3Rjb250b3VyghhlbnZveS5wcm9qZWN0Y29udG91ci5z +dmOCJmVudm95LnByb2plY3Rjb250b3VyLnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqG +SIb3DQEBCwUAA4IBAQBIKpBD1T9tugzJF7lajbdulXTb9qGibwQALqauskX9Sq57 +po/R2TjyxywLn4DgM7BAzzu9qfHWf+S4eQjRUHQshPbUEX9CEsSd5tCu8ZHVbBds +6qFagl2+YQ9ng0Xwta9ezvctM3T6Dy9Kkf5OOe9ysMEsBX7s8NFxe68Qku+cExr3 +78oERlIoNOlT0cNbFLAlH2svNv1uB4qOThRDha52L+mlUdZfTMYZAwNDJWm52t/M +NCIm5NJ5jAJpcJmoEb+JMP3j0x6wydHDXFtGm3WRggZRcrjasyodSKK6szbf96+9 +6syzAwvg9xxNtFxwbhRqqplMEz2sDWaggTrxCQzd +-----END CERTIFICATE----- +` + +var chain4 = `-----BEGIN CERTIFICATE----- +Oy1is0whDArLNwIDAQABo4G9MIG6 +MA4GA1UdDwEB/wQEAwIE8DAdBgNVHQ4EFgQUbAavOY+vIXgc44k8GvLHH6mdzzYw +HwYDVR0jBBgwFoAUb+S3Mu7cbwZiqZaEHTEMyUhLyH4waAYDVR0RBGEwX4IFZW52 +b3mCFGVudm95LnByb2plY3Rjb250b3VyghhlbnZveS5wcm9qZWN0Y29udG91ci5z +dmOCJmVudm95LnByb2plY3Rjb250b3VyLnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqG +SIb3DQEBCwUAA4IBAQBIKpBD1T9tugzJF7lajbdulXTb9qGibwQALqauskX9Sq57 +po/R2TjyxywLn4DgM7BAzzu9qfHWf+S4eQjRUHQshPbUEX9CEsSd5tCu8ZHVbBds +6qFagl2+YQ9ng0Xwta9ezvctM3T6Dy9Kkf5OOe9ysMEsBX7s8NFxe68Qku+cExr3 +78oERlIoNOlT0cNbFLAlH2svNv1uB4qOThRDha52L+mlUdZfTMYZAwNDJWm52t/M +NCIm5NJ5jAJpcJmoEb+JMP3j0x6wydHDXFtGm3WRggZRcrjasyodSKK6szbf96+9 +6syzAwvg9xxNtFxwbhRqqplMEz2sDWaggTrxCQzd +-----END CERTIFICATE----- +` + +// tests validate that the certParser function correctly parses a certificate +func TestCertParser(t *testing.T) { + + expiredCert := []byte(chain) + multiCert := []byte(chain2) + validCert := []byte(chain3) + nonCert := []byte(chain4) + //add docs + + certParserTests := []struct { + Name string + Certs []byte + CertQty int + IsSubject bool + IsCert bool + IsValid bool + }{ + // Name, Certs, CertQty, IsSubject, IsCert, IsValid + {"Widgits", expiredCert, 1, true, true, false}, + {"digicert", multiCert, 2, true, true, false}, //IsValid will not be evaluated for multicerts + {"envoy", validCert, 1, true, true, true}, + {"non.crt", nonCert, 1, false, false, false}, // IsSubject and IsValid n/a for non.crt + } + + for _, e := range certParserTests { + + results, _ := CertParser(e.Name, e.Certs) + + // tests if results contains a list of parsed certificates + var isCert bool + if len(results) == 0 { + isCert = false + } else { + isCert = true + } + + if isCert != e.IsCert { + t.Errorf("Expected %v, but got %v that %s is a certificate", e.IsCert, isCert, e.Name) + continue + } + + for _, cert := range results { + + t.Log(e.Name, cert.Subject) + + // test checks if certificate subject contains a matching string; validates that can parse cert and pull back information + if !strings.Contains(cert.Subject, e.Name) { + t.Error("unable to parse certificate: ", e.Name) + + } else { + isSubject := true + if e.IsSubject != isSubject { + t.Errorf("You expected that %s certificate contains %s in the Subject line but it was not present; here is the entire subject string: %v", e.Name, e.Name, cert.Subject) + } + } + + // test checks for expected quantity of certificates in a certificate file + numCerts := len(results) + if numCerts != e.CertQty { + t.Errorf("expected %d certificates in slice but got %d", e.CertQty, numCerts) + } + if numCerts > 1 { + continue + } + + // test checks for expected certificate expired + if cert.IsValid != e.IsValid { + t.Errorf("When checing that %v certificate is expired, you expected %v, but got %v ", e.Name, e.IsValid, cert.IsValid) + } + + } + + } + +} + +// validates that certificate count is correct when parsing a certificate input string. +func Test_decodePem(t *testing.T) { + certDecoderTests := []struct { + Name string + certInput string + certCount int + }{ + {"widgets-cert", chain, 1}, + {"digi-cert", chain2, 2}, + } + for _, e := range certDecoderTests { + results, _ := decodePem(e.certInput) + certCount := 0 + + for _, cert := range results.Certificate { + t.Log(cert) + certCount++ + } + if certCount != e.certCount { + t.Errorf("cert count -- expected %d, but got %d", e.certCount, certCount) + } + + } +} diff --git a/pkg/collect/collector.go b/pkg/collect/collector.go index 72ffde1c..90d6c08c 100644 --- a/pkg/collect/collector.go +++ b/pkg/collect/collector.go @@ -101,6 +101,8 @@ func GetCollector(collector *troubleshootv1beta2.Collect, bundlePath string, nam return &CollectRegistry{collector.RegistryImages, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true case collector.Sysctl != nil: return &CollectSysctl{collector.Sysctl, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.Certificates != nil: + return &CollectCertificates{collector.Certificates, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true default: return nil, false } @@ -176,6 +178,9 @@ func getCollectorName(c interface{}) string { case *CollectSysctl: collector = "sysctl" name = v.Collector.Name + case *CollectCertificates: + collector = "certificates" + name = v.Collector.Name default: collector = "" } diff --git a/schemas/collector-troubleshoot-v1beta2.json b/schemas/collector-troubleshoot-v1beta2.json index e0c8b7fb..69975137 100644 --- a/schemas/collector-troubleshoot-v1beta2.json +++ b/schemas/collector-troubleshoot-v1beta2.json @@ -88,6 +88,38 @@ } } }, + "certificates": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "configMaps": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "name": { + "type": "string" + }, + "secrets": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, "clusterInfo": { "type": "object", "properties": { diff --git a/schemas/preflight-troubleshoot-v1beta2.json b/schemas/preflight-troubleshoot-v1beta2.json index 8a9f3ffa..162e38f5 100644 --- a/schemas/preflight-troubleshoot-v1beta2.json +++ b/schemas/preflight-troubleshoot-v1beta2.json @@ -2312,6 +2312,38 @@ } } }, + "certificates": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "configMaps": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "name": { + "type": "string" + }, + "secrets": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, "clusterInfo": { "type": "object", "properties": { diff --git a/schemas/supportbundle-troubleshoot-v1beta2.json b/schemas/supportbundle-troubleshoot-v1beta2.json index 170b19df..d646288e 100644 --- a/schemas/supportbundle-troubleshoot-v1beta2.json +++ b/schemas/supportbundle-troubleshoot-v1beta2.json @@ -2358,6 +2358,38 @@ } } }, + "certificates": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "configMaps": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "name": { + "type": "string" + }, + "secrets": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, "clusterInfo": { "type": "object", "properties": {