Merge pull request #326 from areed/host-cert

Analyze TLS certificate
This commit is contained in:
Andrew Reed
2021-02-19 16:30:27 -05:00
committed by GitHub
10 changed files with 581 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: certificate
spec:
collectors:
- certificate:
certificatePath: /etc/ssl/corp.crt
keyPath: /etc/ssl/corp.key
analyzers:
- certificate:
outcomes:
- fail:
when: "key-pair-missing"
message: Certificate key pair not found in /etc/ssl
- fail:
when: "key-pair-switched"
message: Cert and key pair are switched
- fail:
when: "key-pair-encrypted"
message: Private key is encrypted
- fail:
when: "key-pair-mismatch"
message: Cert and key do not match
- fail:
when: "key-pair-invalid"
message: Certificate key pair is invalid
- pass:
when: "key-pair-valid"
message: Certificate key pair is valid
+23
View File
@@ -5,6 +5,9 @@ metadata:
spec:
collectors:
- blockDevices: {}
- certificate:
certificatePath: /etc/ssl/corp.crt
keyPath: /etc/ssl/corp.key
- cpu: {}
- diskUsage:
collectorName: ephemeral
@@ -50,6 +53,26 @@ spec:
message: Multiple available block devices
- fail:
message: No available block devices
- certificate:
outcomes:
- fail:
when: "key-pair-missing"
message: Certificate key pair not found in /etc/ssl
- fail:
when: "key-pair-switched"
message: Cert and key pair are switched
- fail:
when: "key-pair-encrypted"
message: Private key is encrypted
- fail:
when: "key-pair-mismatch"
message: Cert and key do not match
- fail:
when: "key-pair-invalid"
message: Certificate key pair is invalid
- pass:
when: "key-pair-valid"
message: Certificate key pair is valid
- cpu:
outcomes:
- fail:
+7
View File
@@ -125,6 +125,13 @@ func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getColle
}
return []*AnalyzeResult{result}, nil
}
if hostAnalyzer.Certificate != nil {
result, err := analyzeHostCertificate(hostAnalyzer.Certificate, getFile)
if err != nil {
return nil, err
}
return []*AnalyzeResult{result}, nil
}
return nil, errors.New("invalid analyzer")
}
+59
View File
@@ -0,0 +1,59 @@
package analyzer
import (
"path/filepath"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
func analyzeHostCertificate(hostAnalyzer *troubleshootv1beta2.CertificateAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
collectorName := hostAnalyzer.CollectorName
if collectorName == "" {
collectorName = "certificate"
}
name := filepath.Join("certificate", collectorName+".json")
contents, err := getCollectedFileContents(name)
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
}
status := string(contents)
result := AnalyzeResult{}
title := hostAnalyzer.CheckName
if title == "" {
title = "Certificate Key Pair"
}
result.Title = title
for _, outcome := range hostAnalyzer.Outcomes {
if outcome.Fail != nil {
if outcome.Fail.When == "" || outcome.Fail.When == status {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI
return &result, nil
}
} else if outcome.Warn != nil {
if outcome.Warn.When == "" || outcome.Warn.When == status {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
return &result, nil
}
} else if outcome.Pass != nil {
if outcome.Pass.When == "" || outcome.Pass.When == status {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return &result, nil
}
}
}
return &result, nil
}
+333
View File
@@ -0,0 +1,333 @@
package analyzer
import (
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAnalyzeCertificate(t *testing.T) {
tests := []struct {
name string
status string
hostAnalyzer *troubleshootv1beta2.CertificateAnalyze
result *AnalyzeResult
expectErr bool
}{
{
name: "key-pair-valid",
status: collect.KeyPairValid,
hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-missing",
Message: "Certificate key pair not found",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-switched",
Message: "Public and private keys switched",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-encrypted",
Message: "Private key is encrypted",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-mismatch",
Message: "Public and private keys don't match",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-invalid",
Message: "Certificate key pair is invalid",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-valid",
Message: "Certificate key pair is valid",
},
},
},
},
result: &AnalyzeResult{
Title: "Certificate Key Pair",
IsPass: true,
Message: "Certificate key pair is valid",
},
},
{
name: "key-pair-invalid",
status: collect.KeyPairInvalid,
hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-missing",
Message: "Certificate key pair not found",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-switched",
Message: "Public and private keys switched",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-encrypted",
Message: "Private key is encrypted",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-mismatch",
Message: "Public and private keys don't match",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-invalid",
Message: "Certificate key pair is invalid",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-valid",
Message: "Certificate key pair is valid",
},
},
},
},
result: &AnalyzeResult{
Title: "Certificate Key Pair",
IsFail: true,
Message: "Certificate key pair is invalid",
},
},
{
name: "key-pair-missing",
status: collect.KeyPairMissing,
hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-missing",
Message: "Certificate key pair not found",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-switched",
Message: "Public and private keys switched",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-encrypted",
Message: "Private key is encrypted",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-mismatch",
Message: "Public and private keys don't match",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-invalid",
Message: "Certificate key pair is invalid",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-valid",
Message: "Certificate key pair is valid",
},
},
},
},
result: &AnalyzeResult{
Title: "Certificate Key Pair",
IsFail: true,
Message: "Certificate key pair not found",
},
},
{
name: "key-pair-switched",
status: collect.KeyPairSwitched,
hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-missing",
Message: "Certificate key pair not found",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-switched",
Message: "Public and private keys switched",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-encrypted",
Message: "Private key is encrypted",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-mismatch",
Message: "Public and private keys don't match",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-invalid",
Message: "Certificate key pair is invalid",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-valid",
Message: "Certificate key pair is valid",
},
},
},
},
result: &AnalyzeResult{
Title: "Certificate Key Pair",
IsFail: true,
Message: "Public and private keys switched",
},
},
{
name: "key-pair-encrypted",
status: collect.KeyPairEncrypted,
hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-missing",
Message: "Certificate key pair not found",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-switched",
Message: "Public and private keys switched",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-encrypted",
Message: "Private key is encrypted",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-mismatch",
Message: "Public and private keys don't match",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-invalid",
Message: "Certificate key pair is invalid",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-valid",
Message: "Certificate key pair is valid",
},
},
},
},
result: &AnalyzeResult{
Title: "Certificate Key Pair",
IsFail: true,
Message: "Private key is encrypted",
},
},
{
name: "key-pair-mismatch",
status: collect.KeyPairMismatch,
hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-missing",
Message: "Certificate key pair not found",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-switched",
Message: "Public and private keys switched",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-encrypted",
Message: "Private key is mismatch",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-mismatch",
Message: "Public and private keys don't match",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-invalid",
Message: "Certificate key pair is invalid",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-valid",
Message: "Certificate key pair is valid",
},
},
},
},
result: &AnalyzeResult{
Title: "Certificate Key Pair",
IsFail: true,
Message: "Public and private keys don't match",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
getCollectedFileContents := func(filename string) ([]byte, error) {
return []byte(test.status), nil
}
result, err := analyzeHostCertificate(test.hostAnalyzer, getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}
@@ -67,6 +67,12 @@ type FilesystemPerformanceAnalyze struct {
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type CertificateAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type HostAnalyze struct {
CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"`
//
@@ -90,4 +96,6 @@ type HostAnalyze struct {
IPV4Interfaces *IPV4InterfacesAnalyze `json:"ipv4Interfaces" yaml:"ipv4Interfaces"`
FilesystemPerformance *FilesystemPerformanceAnalyze `json:"filesystemPerformance" yaml:"filesystemPerformance"`
Certificate *CertificateAnalyze `json:"certificate" yaml:"certificate"`
}
@@ -82,6 +82,12 @@ type FilesystemPerformance struct {
Datasync bool `json:"datasync,omitempty"`
}
type Certificate struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
CertificatePath string `json:"certificatePath" yaml:"certificatepath"`
KeyPath string `json:"keyPath" yaml:"keyPath"`
}
type HostCollect struct {
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
@@ -96,6 +102,7 @@ type HostCollect struct {
BlockDevices *HostBlockDevices `json:"blockDevices,omitempty" yaml:"blockDevices,omitempty"`
TCPConnect *TCPConnect `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"`
FilesystemPerformance *FilesystemPerformance `json:"filesystemPerformance" yaml:"filesystemPerformance"`
Certificate *Certificate `json:"certificate" yaml:"certificate" yaml:"certificate"`
}
func (c *HostCollect) GetName() string {
@@ -400,6 +400,49 @@ func (in *CephStatusAnalyze) DeepCopy() *CephStatusAnalyze {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Certificate) DeepCopyInto(out *Certificate) {
*out = *in
out.HostCollectorMeta = in.HostCollectorMeta
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Certificate.
func (in *Certificate) DeepCopy() *Certificate {
if in == nil {
return nil
}
out := new(Certificate)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CertificateAnalyze) DeepCopyInto(out *CertificateAnalyze) {
*out = *in
out.AnalyzeMeta = in.AnalyzeMeta
if in.Outcomes != nil {
in, out := &in.Outcomes, &out.Outcomes
*out = make([]*Outcome, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(Outcome)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateAnalyze.
func (in *CertificateAnalyze) DeepCopy() *CertificateAnalyze {
if in == nil {
return nil
}
out := new(CertificateAnalyze)
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
@@ -1230,6 +1273,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
*out = new(FilesystemPerformanceAnalyze)
(*in).DeepCopyInto(*out)
}
if in.Certificate != nil {
in, out := &in.Certificate, &out.Certificate
*out = new(CertificateAnalyze)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze.
@@ -1326,6 +1374,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
*out = new(FilesystemPerformance)
**out = **in
}
if in.Certificate != nil {
in, out := &in.Certificate, &out.Certificate
*out = new(Certificate)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.
+59
View File
@@ -0,0 +1,59 @@
package collect
import (
"bytes"
"crypto/tls"
"io/ioutil"
"path/filepath"
"strings"
)
const KeyPairMissing = "key-pair-missing"
const KeyPairSwitched = "key-pair-switched"
const KeyPairEncrypted = "key-pair-encrypted"
const KeyPairMismatch = "key-pair-mismatch"
const KeyPairInvalid = "key-pair-invalid"
const KeyPairValid = "key-pair-valid"
func HostCertificate(c *HostCollector) (map[string][]byte, error) {
var result = KeyPairValid
_, err := tls.LoadX509KeyPair(c.Collect.Certificate.CertificatePath, c.Collect.Certificate.KeyPath)
if err != nil {
if strings.Contains(err.Error(), "no such file") {
result = KeyPairMissing
} else if strings.Contains(err.Error(), "PEM inputs may have been switched") {
result = KeyPairSwitched
} else if strings.Contains(err.Error(), "found a certificate rather than a key") {
result = KeyPairSwitched
} else if strings.Contains(err.Error(), "private key does not match public key") {
result = KeyPairMismatch
} else if strings.Contains(err.Error(), "failed to parse private key") {
if encrypted, _ := isEncryptedKey(c.Collect.Certificate.KeyPath); encrypted {
result = KeyPairEncrypted
} else {
result = KeyPairInvalid
}
} else {
result = KeyPairInvalid
}
}
collectorName := c.Collect.Certificate.CollectorName
if collectorName == "" {
collectorName = "certificate"
}
name := filepath.Join("certificate", collectorName+".json")
return map[string][]byte{
name: []byte(result),
}, nil
}
func isEncryptedKey(filename string) (bool, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return false, err
}
return bytes.Contains(data, []byte("ENCRYPTED")), nil
}
+2
View File
@@ -42,6 +42,8 @@ func (c *HostCollector) RunCollectorSync() (result map[string][]byte, err error)
result, err = HostIPV4Interfaces(c)
} else if c.Collect.FilesystemPerformance != nil {
result, err = HostFilesystemPerformance(c)
} else if c.Collect.Certificate != nil {
result, err = HostCertificate(c)
} else {
err = errors.New("no spec found to run")
return