diff --git a/.gitignore b/.gitignore index 470be276..ab010f3f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ __pycache__ *.debug coverage.html /kubeconfig* +passwords.yml +/x509-certificate-exporter diff --git a/cmd/x509-certificate-exporter/main.go b/cmd/x509-certificate-exporter/main.go index 0531241b..5ae21a03 100644 --- a/cmd/x509-certificate-exporter/main.go +++ b/cmd/x509-certificate-exporter/main.go @@ -75,6 +75,9 @@ func main() { kubeExcludeLabels := stringArrayFlag{} getopt.FlagLong(&kubeExcludeLabels, "exclude-label", 0, "removes the kube secrets with the given label (or label value if specified) from the watch list (applied after --include-label)") + var PasswordsFile string + getopt.FlagLong(&PasswordsFile, "passwords-file", 0, "path to a yaml file containing a list of passwords to try when opening a p12 file") + getopt.Parse() if *help { @@ -145,6 +148,7 @@ func main() { KubeExcludeNamespaces: kubeExcludeNamespaces, KubeIncludeLabels: kubeIncludeLabels, KubeExcludeLabels: kubeExcludeLabels, + PasswordsFile: PasswordsFile, } if getopt.Lookup("expose-labels").Seen() { diff --git a/go.mod b/go.mod index a9e0e89a..b9a031f0 100644 --- a/go.mod +++ b/go.mod @@ -16,10 +16,12 @@ require ( github.com/stretchr/testify v1.10.0 github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 go.uber.org/automaxprocs v1.6.0 + gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.32.0 k8s.io/apimachinery v0.32.0 k8s.io/client-go v0.32.0 + software.sslmate.com/src/go-pkcs12 v0.5.0 ) require ( @@ -72,7 +74,6 @@ require ( google.golang.org/protobuf v1.35.2 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect diff --git a/go.sum b/go.sum index caa03f23..8c300594 100644 --- a/go.sum +++ b/go.sum @@ -218,3 +218,5 @@ sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aN sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= +software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/internal/certificate.go b/internal/certificate.go index 0a0c855d..98b75e3c 100644 --- a/internal/certificate.go +++ b/internal/certificate.go @@ -14,6 +14,7 @@ import ( "github.com/yalp/jsonpath" "gopkg.in/yaml.v3" v1 "k8s.io/api/core/v1" + "software.sslmate.com/src/go-pkcs12" ) // YAMLCertRef : Contains information to access certificates in yaml files @@ -69,6 +70,8 @@ type certificateRef struct { kubeSecret v1.Secret kubeConfigMap v1.ConfigMap kubeSecretKey string + password string + } type parsedCertificate struct { @@ -89,6 +92,7 @@ const ( certificateFormatYAML = iota certificateFormatKubeSecret = iota certificateFormatKubeConfigMap = iota + certificateFormatP12 = iota ) func (cert *certificateRef) parse() error { @@ -103,6 +107,8 @@ func (cert *certificateRef) parse() error { cert.certificates, err = readAndParseKubeSecret(&cert.kubeSecret, cert.kubeSecretKey) case certificateFormatKubeConfigMap: cert.certificates, err = readAndParseKubeConfigMap(&cert.kubeConfigMap, cert.kubeSecretKey) + case certificateFormatP12: + cert.certificates, err = readAndParsePasswordPkcsFile(cert.path, cert.password) } return err } @@ -126,6 +132,32 @@ func readAndParsePEMFile(path string) ([]*parsedCertificate, error) { return output, nil } +func readAndParsePasswordPkcsFile(path string, password string) ([]*parsedCertificate, error) { + contents, err := readFile(path) + if err != nil { + return nil, err + } + + output := []*parsedCertificate{} + // keystore p12 + _, cert, err := pkcs12.Decode(contents, password) + if err == nil { + output = append(output, &parsedCertificate{cert: cert}) + return output, nil + } + + // truststore p12 + certs, err := pkcs12.DecodeTrustStore(contents, password) + if err != nil { + return nil, err + } + + for _, cert := range certs { + output = append(output, &parsedCertificate{cert: cert}) + } + return output, nil +} + func readAndParseYAMLFile(filePath string, yamlPaths []YAMLCertRef) ([]*parsedCertificate, error) { output := []*parsedCertificate{} diff --git a/internal/exporter.go b/internal/exporter.go index b2e84ee6..7017e651 100644 --- a/internal/exporter.go +++ b/internal/exporter.go @@ -22,6 +22,7 @@ import ( "github.com/bmatcuk/doublestar/v4" "github.com/prometheus/exporter-toolkit/web" + "gopkg.in/yaml.v2" "k8s.io/client-go/kubernetes" ) @@ -52,6 +53,7 @@ type Exporter struct { isDiscovery bool secretsCache *cache.Cache configMapsCache *cache.Cache + PasswordsFile string } type KubeSecretType struct { @@ -285,9 +287,28 @@ func (exporter *Exporter) collectMatchingPaths(pattern string, format certificat continue } + var password string + if strings.HasSuffix(file.Name(), ".p12") || strings.HasSuffix(file.Name(), ".jks") { + format = certificateFormatP12 + password, err = exporter.obtainP12Passwords(path.Clean(path.Join(dir, file.Name()))) + if err != nil { + outputErrors = append(outputErrors, err) + continue + } + } else { + if strings.HasSuffix(file.Name(), ".crt") || + strings.HasSuffix(file.Name(), ".pem") || + strings.HasSuffix(file.Name(), ".cert") { + format = certificateFormatPEM + } else { + continue + } + } + output = append(output, &certificateRef{ - path: path.Clean(path.Join(dir, file.Name())), - format: certificateFormatPEM, + path: path.Clean(path.Join(dir, file.Name())), + format: format, + password: password, }) } } else { @@ -340,6 +361,38 @@ func (exporter *Exporter) collectMatchingPaths(pattern string, format certificat return output, outputErrors } +func (exporter *Exporter) obtainP12Passwords(filename string) (string, error) { + filename = filepath.Base(filename) + if len(exporter.PasswordsFile) == 0 { + return "", errors.New("password file not specified") + } + + passwordsFile, err := os.ReadFile(exporter.PasswordsFile) + if err != nil { + return "", err + } + type P12Config struct { + Name string `yaml:"name"` + Password string `yaml:"password"` + } + type Config struct { + P12 []P12Config `yaml:"pkcs12"` + } + var config Config + if err = yaml.Unmarshal(passwordsFile, &config); err != nil { + return "", err + } + + for _, p12 := range config.P12 { + if p12.Name == filename { + return p12.Password, nil + } + } + + return "", errors.New("p12 password not found") +} + + // compareCertificates compares labels of these two certificates // and returns true if they are the same // It would normally run `.getLabels` on both cert/ref combinations, diff --git a/passwords.yml.example b/passwords.yml.example new file mode 100644 index 00000000..8f7581ea --- /dev/null +++ b/passwords.yml.example @@ -0,0 +1,6 @@ +--- +pkcs12: + - name: "keystore.p12" + password: "mysecretpassword1" + - name: "truststore.p12" + password: "mysecretpassword2"