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

Added checker to extract expiring certificates from cert requests #143

Merged
merged 2 commits into from
Dec 5, 2023
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
39 changes: 21 additions & 18 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@ module github.com/joe-elliott/cert-exporter
go 1.19

require (
github.com/aws/aws-sdk-go v1.27.0
github.com/aws/aws-sdk-go v1.45.7
github.com/bmatcuk/doublestar/v3 v3.0.0
github.com/cert-manager/cert-manager v1.13.0
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/prometheus/client_golang v1.9.0
github.com/prometheus/client_golang v1.16.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.28.3
k8s.io/apimachinery v0.28.3
k8s.io/client-go v0.28.3
software.sslmate.com/src/go-pkcs12 v0.2.0
software.sslmate.com/src/go-pkcs12 v0.2.1
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.10.2 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
Expand All @@ -28,35 +29,37 @@ require (
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.15.0 // indirect
github.com/prometheus/procfs v0.2.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/oauth2 v0.12.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.28.1 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
k8s.io/kube-openapi v0.0.0-20230905202853-d090da108d2f // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/gateway-api v0.8.0 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
454 changes: 64 additions & 390 deletions go.sum

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ var (
awsAccount string
awsRegion string
awsSecrets args.GlobArgs
certRequestsEnabled bool
certRequestsLabelSelector args.GlobArgs
certRequestsAnnotationSelector args.GlobArgs
certRequestsNamespace string
certRequestsListOfNamespaces string
)

func init() {
Expand Down Expand Up @@ -88,6 +93,13 @@ func init() {
flag.StringVar(&awsAccount, "aws-account", "", "AWS account to search for secrets in")
flag.StringVar(&awsRegion, "aws-region", "", "AWS region to search for secrets in")
flag.Var(&awsSecrets, "aws-secret", "AWS secrets to export")

flag.BoolVar(&certRequestsEnabled, "enable-certrequests-check", false, "Enable certrequests check.")
flag.Var(&certRequestsLabelSelector, "certrequests-label-selector", "Label selector to find certrequests to publish as metrics.")
flag.Var(&certRequestsAnnotationSelector, "certrequests-annotation-selector", "Annotation selector to find certrequests to publish as metrics.")
flag.StringVar(&certRequestsNamespace, "certrequests-namespace", "", "Kubernetes namespace to list certrequests.")
flag.StringVar(&certRequestsListOfNamespaces, "certrequests-namespaces", "", "Kubernetes comma-delimited list of namespaces to search for certrequests.")

}

func main() {
Expand Down Expand Up @@ -116,6 +128,14 @@ func main() {
go configChecker.StartChecking()
}

if len(certRequestsLabelSelector) > 0 || len(certRequestsAnnotationSelector) > 0 || certRequestsEnabled {
certRequestNamespaces := getSanitizedNamespaceList(certRequestsListOfNamespaces, certRequestsNamespace)

configChecker := checkers.NewCertRequestChecker(pollingPeriod, certRequestsLabelSelector, secretsAnnotationSelector, certRequestNamespaces, kubeconfigPath, &exporters.CertRequestExporter{})
go configChecker.StartChecking()

}

if len(awsAccount) > 0 && len(awsRegion) > 0 && len(awsSecrets) > 0 {
glog.Infof("Starting check for AWS Secrets Manager in Account %s and Region %s and Secrets %s", awsAccount, awsRegion, awsSecrets)
awsChecker := checkers.NewAwsChecker(awsAccount, awsRegion, awsSecrets, pollingPeriod, &exporters.AwsExporter{})
Expand Down
13 changes: 13 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ cert-exporter can publish metrics about
- direct support for [cert-manager](https://github.com/jetstack/cert-manager)
- configmaps
- [admission webhooks](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)
- cert-manager [CertificateRequest] (https://cert-manager.io/docs/usage/certificaterequest/)
- Certs stored in [AWS Secrets manager](https://aws.amazon.com/secrets-manager/)

See [deployment](./docs/deploy.md) for detailed information on running cert-exporter and examples of running it in a [kops](https://github.com/kubernetes/kops) cluster.
Expand Down Expand Up @@ -62,6 +63,12 @@ cert_exporter_kubeconfig_expires_in_seconds{filename="kubeConfigSibling/kubeconf
# TYPE cert_exporter_secret_expires_in_seconds gauge
cert_exporter_secret_expires_in_seconds{cn="example.com",issuer="example.com",key_name="ca.crt",secret_name="selfsigned-cert-tls",secret_namespace="cert-manager-test"} 8.6396867095666e+06
cert_exporter_secret_expires_in_seconds{cn="example.com",issuer="example.com",key_name="tls.crt",secret_name="selfsigned-cert-tls",secret_namespace="cert-manager-test"} 8.639686709417423e+06
# HELP certrequest_expires_in_seconds Number of seconds til the cert in the CertificateRequest expires.
# TYPE certrequest_expires_in_seconds gauge
cert_exporter_certrequest_expires_in_seconds{cert_request="example-crt-gn762",certrequest_namespace="cert-manager-test",cn="example.com",issuer="example.com"}
# HELP certrequest_not_after_timestamp Timestamp when the cert in the CertificateRequest expires.
# TYPE certrequest_not_after_timestamp gauge
cert_exporter_certrequest_not_after_timestamp{cert_request="example-crt-gn762",certrequest_namespace="cert-manager-test",cn="example.com",issuer="example.com"}
```

**cert_exporter_error_total**
Expand All @@ -76,6 +83,12 @@ The number of seconds until a certificate stored in a kubeconfig expires. The `
**cert_exporter_secret_expires_in_seconds**
The number of seconds until a certificate stored in a kubernetes secret expires. The `key_name`, `issuer`, `cn`, `secret_name`, and `secret_namespace` labels indicate the secret key, name and namespace.

**cert_exporter_certrequest_expires_in_seconds**
The number of seconds until a certificate stored in a cert-manager CertificateRequest expires. The `cert_request`, `issuer`, `cn`, and `certrequest_namespace` labels indicate the CertificateRequest, comon name and namespace.

**cert_exporter_certrequest_not_after_timestamp**
The timestamp when a certificate stored in a cert-manager CertificateRequest expires. The `cert_request`, `issuer`, `cn`, and `certrequest_namespace` labels indicate the CertificateRequest, comon name and namespace.

### Other Docs

- [Testing](./docs/testing.md)
Expand Down
132 changes: 132 additions & 0 deletions src/checkers/periodicCertRequestChecker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package checkers

import (
"context"
"strings"
"time"

cmapiv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
cmClientSet "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned/typed/certmanager/v1"
"github.com/golang/glog"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"

"github.com/joe-elliott/cert-exporter/src/exporters"
"github.com/joe-elliott/cert-exporter/src/metrics"
)

// PeriodicCertRequestChecker is an object designed to check for files on disk at a regular interval
type PeriodicCertRequestChecker struct {
period time.Duration
labelSelectors []string
kubeconfigPath string
annotationSelectors []string
namespaces []string
exporter *exporters.CertRequestExporter
}

// NewCertRequestChecker is a factory method that returns a new PeriodicCertRequestChecker
func NewCertRequestChecker(period time.Duration, labelSelectors, annotationSelectors, namespaces []string, kubeconfigPath string, e *exporters.CertRequestExporter) *PeriodicCertRequestChecker {
return &PeriodicCertRequestChecker{
period: period,
labelSelectors: labelSelectors,
annotationSelectors: annotationSelectors,
namespaces: namespaces,
kubeconfigPath: kubeconfigPath,
exporter: e,
}
}

// StartChecking starts the periodic file check. Most likely you want to run this as an independent go routine.
func (p *PeriodicCertRequestChecker) StartChecking() {
config, err := clientcmd.BuildConfigFromFlags("", p.kubeconfigPath)
if err != nil {
glog.Fatalf("Error building kubeconfig: %s", err.Error())
}

// creates the certmanager client
certmanagerClient, err := cmClientSet.NewForConfig(config)
if err != nil {
glog.Fatalf("certmanager.NewForConfig failed: %v", err)
}

periodChannel := time.Tick(p.period)
if strings.Join(p.namespaces, ", ") != "" {
glog.Infof("Scan certrequests in %v", strings.Join(p.namespaces, ", "))
}
for {
glog.Info("Begin periodic check")

p.exporter.ResetMetrics()

var certrequests []cmapiv1.CertificateRequest

for _, ns := range p.namespaces {
var c *cmapiv1.CertificateRequestList

if len(p.labelSelectors) > 0 {
for _, labelSelector := range p.labelSelectors {
c, err = certmanagerClient.CertificateRequests(ns).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector})
if err != nil {
glog.Errorf("Error requesting certrequest %v", err)
metrics.ErrorTotal.Inc()
continue
}
certrequests = append(certrequests, c.Items...)
}
} else {
c, err = certmanagerClient.CertificateRequests(ns).List(context.TODO(), metav1.ListOptions{})
if err != nil {
glog.Errorf("Error requesting certrequest %v", err)
metrics.ErrorTotal.Inc()
continue
}
certrequests = append(certrequests, c.Items...)
}
}

for _, certrequest := range certrequests {
include := false
for _, condition := range certrequest.Status.Conditions {
// Include only certrequests that issued a certificate successfully
if condition.Type == "Ready" && condition.Status == "True" {
include = true
break
}
}
if !include {
glog.Infof("Ignoring certrequest %s in %s because it is not ready", certrequest.GetName(), certrequest.GetNamespace())
continue
}

glog.Infof("Reviewing certrequest %v in %v", certrequest.GetName(), certrequest.GetNamespace())

if len(p.annotationSelectors) > 0 {
matches := false
annotations := certrequest.GetAnnotations()
for _, selector := range p.annotationSelectors {
_, ok := annotations[selector]
if ok {
matches = true
break
}
}

if !matches {
continue
}
}
glog.Infof("Annotations matched. Parsing certrequest.")

glog.Infof("Publishing %v/%v metrics", certrequest.Name, certrequest.Namespace)
err = p.exporter.ExportMetrics(certrequest.Status.Certificate, certrequest.Name, certrequest.Namespace)
if err != nil {
glog.Errorf("Error exporting certrequest %v", err)
metrics.ErrorTotal.Inc()
}

}

<-periodChannel
}
}
29 changes: 29 additions & 0 deletions src/exporters/certRequestExporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package exporters

import (
"github.com/joe-elliott/cert-exporter/src/metrics"
)

// CertRequestExporter exports PEM file certs
type CertRequestExporter struct {
}

// ExportMetrics exports the provided PEM file
func (c *CertRequestExporter) ExportMetrics(bytes []byte, certrequest, certrequestNamespace string) error {
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.CertRequestNotAfterTimestamp.WithLabelValues(metric.issuer, metric.cn, certrequest, certrequestNamespace).Set(metric.notAfter)
}

return nil
}

func (c *CertRequestExporter) ResetMetrics() {
metrics.SecretExpirySeconds.Reset()
metrics.SecretNotAfterTimestamp.Reset()
}
22 changes: 22 additions & 0 deletions src/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,26 @@ var (
[]string{"key_name", "issuer", "cn", "secret_name", "secret_namespace"},
)

// CertRequestExpirySeconds is a prometheus gauge that indicates the number of seconds until a certificate in a cert-manager certificate request expires
CertRequestExpirySeconds = prometheus.NewGaugeVec(
simonwilli marked this conversation as resolved.
Show resolved Hide resolved
prometheus.GaugeOpts{
Namespace: namespace,
Name: "certrequest_expires_in_seconds",
Help: "Number of seconds til the cert in the certrequest expires.",
},
[]string{"issuer", "cn", "cert_request", "certrequest_namespace"},
)

// CertRequestNotAfterTimestamp is a prometheus gauge that indicates the NotAfter timestamp.
CertRequestNotAfterTimestamp = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Name: "certrequest_not_after_timestamp",
Help: "Expiration timestamp for cert in the certrequest.",
},
[]string{"issuer", "cn", "cert_request", "certrequest_namespace"},
)

// AwsCertExpirySeconds is a prometheus gauge that indicates the number of seconds until certificates on AWS expires.
AwsCertExpirySeconds = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Expand Down Expand Up @@ -141,6 +161,8 @@ func Init(prometheusExporterMetricsDisabled bool) {
prometheus.MustRegister(KubeConfigNotAfterTimestamp)
prometheus.MustRegister(SecretExpirySeconds)
prometheus.MustRegister(SecretNotAfterTimestamp)
prometheus.MustRegister(CertRequestExpirySeconds)
prometheus.MustRegister(CertRequestNotAfterTimestamp)
prometheus.MustRegister(ConfigMapExpirySeconds)
prometheus.MustRegister(ConfigMapNotAfterTimestamp)
prometheus.MustRegister(WebhookExpirySeconds)
Expand Down
14 changes: 14 additions & 0 deletions test/cert-manager/certs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,27 @@ kind: Certificate
metadata:
name: example-crt
namespace: cert-manager-test
labels:
testlabel: test
spec:
commonName: example.com
secretName: selfsigned-cert-tls
duration: 2400h
issuerRef:
name: selfsigning-issuer
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-crt-not-working
namespace: cert-manager-test
spec:
commonName: example.com
secretName: example-crt-not-working
duration: 2400h
issuerRef:
name: notworking-issuer
---
apiVersion: v1
data:
test.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCekNDQWUrZ0F3SUJBZ0lVTDVRNCs0YkdFMkpJSDI1dk9hTEtsNmFmWmJRd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0V6RVJNQThHQTFVRUF3d0lhRzF6TFhSbGMzUXdIaGNOTWpBd016RXpNVEV4TlRVeldoY05NakV3TXpFegpNVEV4TlRVeldqQVRNUkV3RHdZRFZRUUREQWhvYlhNdGRHVnpkRENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFECmdnRVBBRENDQVFvQ2dnRUJBTVVNZDg5di9EbjNBTUVWcHBoL2FETElYdE80eEdZYWtocit2RDVid3RFTmkzOHgKeG5nWWh3WmJwYUkrTkhHZlJxVnhnQTIvUXJQSG9LUHE2eHhMK1VUa3prYVB0aCtZUVJqM3lqYTBud3pTZzZJYgo0ZTIvK3lya2J0M29RK2k5SjlkZ2lLbFdvVk56clV2U3hoRkFBMnlvclEremhmb1JEdHNsR2xScWQ3Qng2K3hvCm1pVUpUZVNYUVh1ZmVZVmxSNExKNThlZjFHaUJ5U2p4MVVBekpYbEE2ek5MVU5BeHZSbEd1WEs3QUc1NmVHRGsKNkxKbURTMHpWNzFQb0EzZmIvUXFTZ1F2U013amlzdzhXWUNoQUxQei9OWXErQTJwM051aHhyTVdETnhNdnFTRQpKdGxySmt2emRmb3VpY2o5cXoyZTNwTmV1bnZtaDY5UU1WdGo1NzBDQXdFQUFhTlRNRkV3SFFZRFZSME9CQllFCkZKanpZb1l0TU9qejFkVVlSSFhJaitCOWF2U3ZNQjhHQTFVZEl3UVlNQmFBRkpqellvWXRNT2p6MWRVWVJIWEkKaitCOWF2U3ZNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSlR2RDdFRgpVd3dLS2VFYVlFelJLSDhWanlSd08xRTFkN3EwVXhDOU9ubC9mTHU0L0RYU20vTCtvbSt5QWsrV2pzVWtmNkdzClZ1dE5sbUNEME5xZWU1RnNWYy82RnlhQUZSUG45UTJlNTNwUERQTVJwSWgwdHRkcXdQdVk3UUhqMUFFclFrRisKMC9ZOFdoTllrQStyVFR5cG10YTB4dXUrc0JXMURrRDliei9HUWpqZm52U0wyVTJIM2Z1L0tycERQM3RKb2NtNApQczBYZ29vK2k2Q0JGY3Q4dEtQYWsrZTVNSGtyTXZVNHZNV1Q1aTlKNzVpWUR3R1BkZWlvSlE3NFlSSXo0SWo1CmdVb3VqemVIN01qK3FpNTl0VHU3bnVRZHlVeEhwR3hlUlJrRmQvZFVqbXVtZDVMbTJTb2JDSDZoeXVBbHVrRC8KWDJWTmI0MXp5Ri8wazA0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
Expand Down
Loading
Loading