Skip to content

Commit

Permalink
Add support for password protected certificates (#151)
Browse files Browse the repository at this point in the history
* Add password support for certs in kubernetes secrets

* Add information about password-protected certificate support

* Test password protected certificates

* Add missing empty password parameter for cert-request export
  • Loading branch information
Blackhawk312 committed Jan 2, 2024
1 parent 489e98c commit bc0b6bf
Show file tree
Hide file tree
Showing 11 changed files with 60 additions and 29 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.vscode
.idea
dist/
test/files/certs
test/files/certsSibling
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ cert-exporter can publish metrics about
- Certs stored in Kubernetes
- secrets
- direct support for [cert-manager](https://github.com/jetstack/cert-manager)
- support for password-protected certificates
- configmaps
- [admission webhooks](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)
- cert-manager [CertificateRequest] (https://cert-manager.io/docs/usage/certificaterequest/)
Expand Down
7 changes: 6 additions & 1 deletion src/checkers/periodicSecretChecker.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,12 @@ func (p *PeriodicSecretChecker) StartChecking() {

if include && !exclude {
glog.Infof("Publishing %v/%v metrics %v", secret.Name, secret.Namespace, name)
err = p.exporter.ExportMetrics(bytes, name, secret.Name, secret.Namespace)
certPassword := ""
if certPasswortBytes, found := secret.Data["password"]; found {
certPassword = string(certPasswortBytes)
}

err = p.exporter.ExportMetrics(bytes, name, secret.Name, secret.Namespace, certPassword)
if err != nil {
glog.Errorf("Error exporting secret %v", err)
metrics.ErrorTotal.Inc()
Expand Down
35 changes: 17 additions & 18 deletions src/exporters/certHelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"crypto/x509"
"encoding/base64"
"encoding/pem"
"io/ioutil"
"os"

"software.sslmate.com/src/go-pkcs12"
"software.sslmate.com/src/go-pkcs12"
)

type certMetric struct {
Expand All @@ -20,12 +20,12 @@ type certMetric struct {
}

func secondsToExpiryFromCertAsFile(file string) ([]certMetric, error) {
certBytes, err := ioutil.ReadFile(file)
certBytes, err := os.ReadFile(file)
if err != nil {
return []certMetric{}, err
}

return secondsToExpiryFromCertAsBytes(certBytes)
return secondsToExpiryFromCertAsBytes(certBytes, "")
}

func secondsToExpiryFromCertAsBase64String(s string) ([]certMetric, error) {
Expand All @@ -34,25 +34,25 @@ func secondsToExpiryFromCertAsBase64String(s string) ([]certMetric, error) {
return []certMetric{}, err
}

return secondsToExpiryFromCertAsBytes(certBytes)
return secondsToExpiryFromCertAsBytes(certBytes, "")
}

func secondsToExpiryFromCertAsBytes(certBytes []byte) ([]certMetric, error) {
func secondsToExpiryFromCertAsBytes(certBytes []byte, certPassword string) ([]certMetric, error) {
var metrics []certMetric

parsed, metrics, err := parseAsPEM(certBytes)
if parsed {
return metrics, err
}
// Parse as PKCS ?
parsed, metrics, err = parseAsPKCS(certBytes)
parsed, metrics, err = parseAsPKCS(certBytes, certPassword)
if parsed {
return metrics, nil
}
return nil, fmt.Errorf("failed to parse as pem and pkcs12: %w", err)
}

func getCertificateMetrics(cert *x509.Certificate)(certMetric) {
func getCertificateMetrics(cert *x509.Certificate) certMetric {
var metric certMetric
metric.notAfter = float64(cert.NotAfter.Unix())
metric.durationUntilExpiry = time.Until(cert.NotAfter).Seconds()
Expand All @@ -61,16 +61,16 @@ func getCertificateMetrics(cert *x509.Certificate)(certMetric) {
return metric
}

func parseAsPKCS(certBytes []byte) (bool, []certMetric, error) {
func parseAsPKCS(certBytes []byte, certPassword string) (bool, []certMetric, error) {
var metrics []certMetric
var blocks []*pem.Block
var last_err error
pfx_blocks, err := pkcs12.ToPEM(certBytes, "")

pfx_blocks, err := pkcs12.ToPEM(certBytes, certPassword)
if err != nil {
return false, nil, err
}
for _ , b := range pfx_blocks {
for _, b := range pfx_blocks {
if b.Type == "CERTIFICATE" {
blocks = append(blocks, b)
}
Expand All @@ -88,14 +88,14 @@ func parseAsPKCS(certBytes []byte) (bool, []certMetric, error) {
return true, metrics, last_err
}

func parseAsPEM(certBytes []byte)(bool, []certMetric, error) {
func parseAsPEM(certBytes []byte) (bool, []certMetric, error) {
var metrics []certMetric
var blocks []*pem.Block

block, rest := pem.Decode(certBytes)
if block == nil {
return false, metrics, fmt.Errorf("Failed to parse as a pem")
}
}
blocks = append(blocks, block)
// Export the remaining certificates in the certificate chain
for len(rest) != 0 {
Expand All @@ -110,11 +110,10 @@ func parseAsPEM(certBytes []byte)(bool, []certMetric, error) {
for _, block := range blocks {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return true, metrics ,err
return true, metrics, err
}
var metric = getCertificateMetrics(cert)
metrics = append(metrics, metric)
}
return true, metrics, nil
}

4 changes: 2 additions & 2 deletions src/exporters/certRequestExporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ type CertRequestExporter struct {

// ExportMetrics exports the provided PEM file
func (c *CertRequestExporter) ExportMetrics(bytes []byte, certrequest, certrequestNamespace string) error {
metricCollection, err := secondsToExpiryFromCertAsBytes(bytes)
metricCollection, err := secondsToExpiryFromCertAsBytes(bytes, "")
if err != nil {
return err
}

for _, metric := range metricCollection {
metrics.CertRequestExpirySeconds.WithLabelValues( metric.issuer, metric.cn, certrequest, certrequestNamespace).Set(metric.durationUntilExpiry)
metrics.CertRequestExpirySeconds.WithLabelValues(metric.issuer, metric.cn, certrequest, certrequestNamespace).Set(metric.durationUntilExpiry)
metrics.CertRequestNotAfterTimestamp.WithLabelValues(metric.issuer, metric.cn, certrequest, certrequestNamespace).Set(metric.notAfter)
}

Expand Down
2 changes: 1 addition & 1 deletion src/exporters/configMapExporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type ConfigMapExporter struct {

// ExportMetrics exports the provided PEM file
func (c *ConfigMapExporter) ExportMetrics(bytes []byte, keyName, configMapName, configMapNamespace string) error {
metricCollection, err := secondsToExpiryFromCertAsBytes(bytes)
metricCollection, err := secondsToExpiryFromCertAsBytes(bytes, "")
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions src/exporters/secretExporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ type SecretExporter struct {
}

// ExportMetrics exports the provided PEM file
func (c *SecretExporter) ExportMetrics(bytes []byte, keyName, secretName, secretNamespace string) error {
metricCollection, err := secondsToExpiryFromCertAsBytes(bytes)
func (c *SecretExporter) ExportMetrics(bytes []byte, keyName, secretName, secretNamespace, certPassword string) error {
metricCollection, err := secondsToExpiryFromCertAsBytes(bytes, certPassword)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion src/exporters/webhookExporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type WebhookExporter struct {

// ExportMetrics exports the provided PEM file
func (c *WebhookExporter) ExportMetrics(bytes []byte, typeName, webhookName, admissionReviewVersionName string) error {
metricCollection, err := secondsToExpiryFromCertAsBytes(bytes)
metricCollection, err := secondsToExpiryFromCertAsBytes(bytes, "")
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions src/kubeconfig/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package kubeconfig

import (
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
)

// KubeConfig is a partial description of a kubeconfig file. It defines only the fields required by this application.
Expand All @@ -27,7 +27,7 @@ type KubeConfig struct {
func ParseKubeConfig(file string) (*KubeConfig, error) {
k := &KubeConfig{}

data, err := ioutil.ReadFile(file)
data, err := os.ReadFile(file)
if err != nil {
return nil, err
}
Expand Down
11 changes: 10 additions & 1 deletion test/cert-manager/certs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ spec:
name: notworking-issuer
---
apiVersion: v1
data:
password: bXlQYXNzd29yZB==
store.p12: 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
kind: Secret
metadata:
name: example-crt-pw
namespace: cert-manager-test
type: Opaque
---
apiVersion: v1
data:
test.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCekNDQWUrZ0F3SUJBZ0lVTDVRNCs0YkdFMkpJSDI1dk9hTEtsNmFmWmJRd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0V6RVJNQThHQTFVRUF3d0lhRzF6TFhSbGMzUXdIaGNOTWpBd016RXpNVEV4TlRVeldoY05NakV3TXpFegpNVEV4TlRVeldqQVRNUkV3RHdZRFZRUUREQWhvYlhNdGRHVnpkRENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFECmdnRVBBRENDQVFvQ2dnRUJBTVVNZDg5di9EbjNBTUVWcHBoL2FETElYdE80eEdZYWtocit2RDVid3RFTmkzOHgKeG5nWWh3WmJwYUkrTkhHZlJxVnhnQTIvUXJQSG9LUHE2eHhMK1VUa3prYVB0aCtZUVJqM3lqYTBud3pTZzZJYgo0ZTIvK3lya2J0M29RK2k5SjlkZ2lLbFdvVk56clV2U3hoRkFBMnlvclEremhmb1JEdHNsR2xScWQ3Qng2K3hvCm1pVUpUZVNYUVh1ZmVZVmxSNExKNThlZjFHaUJ5U2p4MVVBekpYbEE2ek5MVU5BeHZSbEd1WEs3QUc1NmVHRGsKNkxKbURTMHpWNzFQb0EzZmIvUXFTZ1F2U013amlzdzhXWUNoQUxQei9OWXErQTJwM051aHhyTVdETnhNdnFTRQpKdGxySmt2emRmb3VpY2o5cXoyZTNwTmV1bnZtaDY5UU1WdGo1NzBDQXdFQUFhTlRNRkV3SFFZRFZSME9CQllFCkZKanpZb1l0TU9qejFkVVlSSFhJaitCOWF2U3ZNQjhHQTFVZEl3UVlNQmFBRkpqellvWXRNT2p6MWRVWVJIWEkKaitCOWF2U3ZNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSlR2RDdFRgpVd3dLS2VFYVlFelJLSDhWanlSd08xRTFkN3EwVXhDOU9ubC9mTHU0L0RYU20vTCtvbSt5QWsrV2pzVWtmNkdzClZ1dE5sbUNEME5xZWU1RnNWYy82RnlhQUZSUG45UTJlNTNwUERQTVJwSWgwdHRkcXdQdVk3UUhqMUFFclFrRisKMC9ZOFdoTllrQStyVFR5cG10YTB4dXUrc0JXMURrRDliei9HUWpqZm52U0wyVTJIM2Z1L0tycERQM3RKb2NtNApQczBYZ29vK2k2Q0JGY3Q4dEtQYWsrZTVNSGtyTXZVNHZNV1Q1aTlKNzVpWUR3R1BkZWlvSlE3NFlSSXo0SWo1CmdVb3VqemVIN01qK3FpNTl0VHU3bnVRZHlVeEhwR3hlUlJrRmQvZFVqbXVtZDVMbTJTb2JDSDZoeXVBbHVrRC8KWDJWTmI0MXp5Ri8wazA0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
kind: Secret
Expand Down Expand Up @@ -99,4 +109,3 @@ webhooks:
apiGroups: ["core", ""]
apiVersions: ["*"]
resources: ["persistentvolumeclaims"]

18 changes: 17 additions & 1 deletion test/cert-manager/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ set -o errexit

validateMetrics() {
metrics=$1
expectedVal=$2
expectedVal=$2

raw=$(curl --silent http://localhost:8080/metrics | grep "$metrics" || true)

Expand Down Expand Up @@ -107,6 +107,22 @@ validateMetrics 'cert_exporter_secret_expires_in_seconds{cn="example.com",issuer
echo "** Killing $pid"
kill $pid

echo "** Testing Secret checker"
# run exporter
$CERT_EXPORTER_PATH \
--kubeconfig=$CONFIG_PATH \
--secrets-include-glob='*.p12' \
--secrets-namespace='cert-manager-test' \
--logtostderr &
pid=$!
sleep 10

validateMetrics 'cert_exporter_secret_expires_in_seconds{cn="",issuer="",key_name="store.p12",secret_name="example-crt-pw",secret_namespace="cert-manager-test"}'

# kill exporter
echo "** Killing $pid"
kill $pid

echo "** Testing ConfigMap checker"
# run exporter
$CERT_EXPORTER_PATH \
Expand Down

0 comments on commit bc0b6bf

Please sign in to comment.