Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for password protected certificates #151

Merged
merged 5 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
joe-elliott marked this conversation as resolved.
Show resolved Hide resolved
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
Loading