From 5264a07003d0f2f872e1c43dc37fea6051b24dc7 Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 15:48:47 -0400 Subject: [PATCH 01/13] feat!: support CRLs with multiple issuers Resolves: https://github.com/aarnaud/vault-pki-exporter/issues/19 It is common practice to have multiple issuers and therefore CRLs in a single Vault PKI secrets engine. Previously we'd only have metrics for the default issuer in a PKI secrets engine. This refactors metrics gathering to support CRL related metrics for all issuers in a secrets engine. This is marked as a breaking change due to the *addition* of an `issuer` label to metrics like `x509_crl_length` but in most cases adding a new label to existing metrics won't break alerting or recording rules compared to removing a label. Regardless proper notice is given. Old metric example: ``` x509_crl_nextupdate{source="pki/"} 1.730633058e+09 ``` New metric example: ``` x509_crl_nextupdate{issuer="CN=my-website.com",source="pki/"} 1.730633058e+09 x509_crl_nextupdate{issuer="CN=mysecondwebsite.com",source="pki/"} 1.730633058e+09 ``` --- README.md | 4 +- pkg/vault-mon/influx.go | 6 ++- pkg/vault-mon/pki.go | 105 +++++++++++++++++++++++++++++++----- pkg/vault-mon/prometheus.go | 29 +++++----- pkg/vault/client.go | 4 +- tests.yml | 4 +- vault-setup.sh | 15 +++++- 7 files changed, 134 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 147f0e9..26be1bb 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,10 @@ x509_cert,common_name=My\ PKI\ CA,country=CA,host=your.hostname.com,locality=Mon ```console # HELP x509_crl_expiry # TYPE x509_crl_expiry gauge -x509_crl_expiry{source="pki-test/"} 243687.999819847 +x509_crl_expiry{source="pki-test/", issuer="CN=example.com"} 243687.999819847 # HELP x509_crl_nextupdate # TYPE x509_crl_nextupdate gauge -x509_crl_nextupdate{source="pki-test/"} 1.573235993e+09 +x509_crl_nextupdate{source="pki-test/", issuer="CN=example.com"} 1.573235993e+09 # HELP x509_cert_age # TYPE x509_cert_age gauge x509_cert_age{common_name="My PKI CA",country="CA",locality="Montreal",organization="Example",organizational_unit="WebService",province="QC",serial="0e-50-38-4d-18-69-52-54-1d-71-31-49-1b-a8-06-c7-4f-23-64-26",source="pki-test/"} 15543.000180153 diff --git a/pkg/vault-mon/influx.go b/pkg/vault-mon/influx.go index 59baa9f..0047239 100644 --- a/pkg/vault-mon/influx.go +++ b/pkg/vault-mon/influx.go @@ -36,8 +36,10 @@ func InfluxWatchCerts(pkimon *PKIMon, interval time.Duration, loop bool) { func influxProcessData(pkimon *PKIMon) { for pkiname, pki := range pkimon.GetPKIs() { - if crl := pki.GetCRL(); crl != nil { - printCrlInfluxPoint(pkiname, crl) + for _, crl := range pki.GetCRLs() { + if crl != nil { + printCrlInfluxPoint(pkiname, crl) + } } for _, cert := range pki.GetCerts() { printCertificateInfluxPoint(pkiname, cert) diff --git a/pkg/vault-mon/pki.go b/pkg/vault-mon/pki.go index b9d08ea..4e34b2e 100644 --- a/pkg/vault-mon/pki.go +++ b/pkg/vault-mon/pki.go @@ -20,7 +20,7 @@ import ( type PKI struct { path string certs map[string]*x509.Certificate - crl *pkix.CertificateList + crls map[string]*pkix.CertificateList crlRawSize int expiredCertsCounter int vault *vaultapi.Client @@ -111,25 +111,104 @@ func (mon *PKIMon) GetPKIs() map[string]*PKI { func (pki *PKI) loadCrl() error { pki.crlmux.Lock() defer pki.crlmux.Unlock() - secret, err := pki.vault.Logical().Read(fmt.Sprintf("%scert/crl", pki.path)) + + // Step 1: List all issuers + issuers, err := pki.listIssuers() if err != nil { return err } - secretCert := vault.SecretCertificate{} - err = mapstructure.Decode(secret.Data, &secretCert) + + if pki.crls == nil { + pki.crls = make(map[string]*pkix.CertificateList) + log.Warningln("init an empty certs list") + } + // Step 2: Load CRL for each issuer + for _, issuerRef := range issuers { + crl, err := pki.loadCrlForIssuer(issuerRef) + if err != nil { + // Handle or log the error + log.Errorf("loadCrl() failed to load CRL for issuer %s, error: %v", issuerRef, err) + // Decide whether to return error or continue loading other CRLs + } else { + pki.crls[issuerRef] = crl + } + } + + return nil +} + +func (pki *PKI) listIssuers() ([]string, error) { + // Define the path where the issuers are listed in your Vault setup + // This path might be different based on your Vault configuration + + // Make a read request to Vault + secret, err := pki.vault.Logical().List(fmt.Sprintf("%s/issuers", pki.path)) if err != nil { - return err + return nil, fmt.Errorf("error listing issuers: %w", err) + } + + if secret == nil || secret.Data == nil { + return []string{}, nil + } + + // Extract the list of issuers from the response + // The key under which issuers Ware listed might vary, so adjust "keys" accordingly + issuerRefs, ok := secret.Data["keys"].([]interface{}) + if !ok { + return nil, fmt.Errorf("failed to parse issuer list") + } + + // Convert issuer references to a slice of strings + issuers := make([]string, len(issuerRefs)) + for i, ref := range issuerRefs { + issuer, ok := ref.(string) + if !ok { + return nil, fmt.Errorf("invalid issuer reference type") + } + issuers[i] = issuer + } + + return issuers, nil +} + +func (pki *PKI) loadCrlForIssuer(issuerRef string) (*pkix.CertificateList, error) { + secret, err := pki.vault.Logical().Read(fmt.Sprintf("/%s/issuer/%s/crl", pki.path, issuerRef)) + if err != nil { + return nil, fmt.Errorf("error finding CRL at /%s/issuer/%s/crl: %w", pki.path, issuerRef, err) + } + + if secret == nil { + log.Errorf("No secret found for issuer %s", issuerRef) + return nil, fmt.Errorf("no secret found for issuer %s", issuerRef) } - block, _ := pem.Decode([]byte([]byte(secretCert.Certificate))) - pki.crlRawSize = len([]byte(secretCert.Certificate)) + + log.Infof("Issuer: %s, Secret Data: %v", issuerRef, secret.Data) + + crlData, ok := secret.Data["crl"].(string) + if !ok || crlData == "" { + log.Errorf("CRL data missing or invalid for issuer %s", issuerRef) + return nil, fmt.Errorf("crl data missing or invalid for issuer %s", issuerRef) + } + + log.Infof("Issuer: %s, Raw CRL Data: %s", issuerRef, crlData) + + block, _ := pem.Decode([]byte(crlData)) + if block == nil { + log.Errorf("Failed to parse PEM block for issuer %s. Raw PEM data: %s", issuerRef, crlData) + return nil, fmt.Errorf("failed to parse PEM block for issuer %s", issuerRef) + } + + pki.crlRawSize = len([]byte(crlData)) + crl, err := x509.ParseCRL(block.Bytes) if err != nil { - log.Errorf("failed to load CRL for %s, error: %w", pki.path, err) - return err + log.Errorf("Error parsing CRL for issuer %s: %v", issuerRef, err) + return nil, fmt.Errorf("error parsing CRL for issuer %s: %w", issuerRef, err) } - pki.crl = crl - return nil + log.Infof("Successfully loaded CRL for issuer %s", issuerRef) + + return crl, nil } func (pki *PKI) loadCerts() error { @@ -248,10 +327,10 @@ func (pki *PKI) clearCerts() { pki.certsmux.Unlock() } -func (pki *PKI) GetCRL() *pkix.CertificateList { +func (pki *PKI) GetCRLs() map[string]*pkix.CertificateList { pki.crlmux.Lock() defer pki.crlmux.Unlock() - return pki.crl + return pki.crls } func (pki *PKI) GetCerts() map[string]*x509.Certificate { diff --git a/pkg/vault-mon/prometheus.go b/pkg/vault-mon/prometheus.go index 051f737..269d2c4 100644 --- a/pkg/vault-mon/prometheus.go +++ b/pkg/vault-mon/prometheus.go @@ -55,18 +55,18 @@ func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { }, []string{"source"}) crl_expiry := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_crl_expiry", - }, []string{"source"}) + }, []string{"source", "issuer"}) crl_nextupdate := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_crl_nextupdate", - }, []string{"source"}) + }, []string{"source", "issuer"}) crl_byte_size := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_crl_byte_size", Help: "Size of raw certificate revocation list pem stored in vault", - }, []string{"source"}) + }, []string{"source", "issuer"}) crl_length := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_crl_length", Help: "Length of certificate revocation list", - }, []string{"source"}) + }, []string{"source", "issuer"}) promWatchCertsDuration := promauto.NewHistogram(prometheus.HistogramOpts{ Name: "x509_watch_certs_duration_seconds", Help: "Duration of promWatchCerts execution", @@ -80,14 +80,19 @@ func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { revokedCerts := make(map[string]struct{}) for pkiname, pki := range pkis { - if crl := pki.GetCRL(); crl != nil { - crl_expiry.WithLabelValues(pkiname).Set(float64(crl.TBSCertList.NextUpdate.Sub(now).Seconds())) - crl_nextupdate.WithLabelValues(pkiname).Set(float64(crl.TBSCertList.NextUpdate.Unix())) - crl_length.WithLabelValues(pkiname).Set(float64(len(crl.TBSCertList.RevokedCertificates))) - crl_byte_size.WithLabelValues(pkiname).Set(float64(pki.crlRawSize)) - // gather revoked certs from the CRL so we can exclude their metrics later - for _, revokedCert := range crl.TBSCertList.RevokedCertificates { - revokedCerts[revokedCert.SerialNumber.String()] = struct{}{} + for _, crl := range pki.GetCRLs() { + if crl != nil { + // issuer string is vanity, such as CN=my-website.com + issuer := crl.TBSCertList.Issuer.String() + + crl_expiry.WithLabelValues(pkiname, issuer).Set(float64(crl.TBSCertList.NextUpdate.Sub(now).Seconds())) + crl_nextupdate.WithLabelValues(pkiname, issuer).Set(float64(crl.TBSCertList.NextUpdate.Unix())) + crl_length.WithLabelValues(pkiname, issuer).Set(float64(len(crl.TBSCertList.RevokedCertificates))) + crl_byte_size.WithLabelValues(pkiname, issuer).Set(float64(pki.crlRawSize)) + // gather revoked certs from the CRL so we can exclude their metrics later + for _, revokedCert := range crl.TBSCertList.RevokedCertificates { + revokedCerts[revokedCert.SerialNumber.String()] = struct{}{} + } } } for _, cert := range pki.GetCerts() { diff --git a/pkg/vault/client.go b/pkg/vault/client.go index ce8a033..b1136cf 100644 --- a/pkg/vault/client.go +++ b/pkg/vault/client.go @@ -116,11 +116,11 @@ func (vault *ClientWrapper) GetSecret(path string, fn secretCallback) error { time.Sleep(time.Duration(secret.LeaseDuration) * time.Second) secret, err = vault.Client.Logical().Read(path) if err != nil { - log.Errorln("[vault]", err) + log.Error("[vault]", err) continue } if secret == nil { - log.Errorln("[vault] secret not found : %s", path) + log.Errorf("[vault] secret not found : %s", path) continue } fn(secret) diff --git a/tests.yml b/tests.yml index fa2ecd6..37ff3d8 100644 --- a/tests.yml +++ b/tests.yml @@ -323,7 +323,9 @@ testcases: - result.body ShouldContainSubstring common_name="*.second-ca.example.com" - result.body ShouldContainSubstring common_name="alt.second-ca.example.com" - result.body ShouldNotContainSubstring common_name="revokeme.first-ca.example.com" - - result.body ShouldContainSubstring x509_crl_length{source="first-ca/"} 1 + # CRLs for each issuer + - result.body ShouldContainSubstring x509_crl_length{issuer="CN=First CA,OU=WebService,O=example,L=Montreal,ST=QC,C=CA",source="first-ca/"} 1 + - result.body ShouldContainSubstring x509_crl_length{issuer="CN=Second CA,OU=VPN,O=example2,L=Montreal,ST=QC,C=CA",source="second-ca/"} 0 - name: docker compose down steps: diff --git a/vault-setup.sh b/vault-setup.sh index c654ff5..bc748e0 100755 --- a/vault-setup.sh +++ b/vault-setup.sh @@ -18,7 +18,7 @@ vault secrets enable pki vault secrets tune -max-lease-ttl=87600h pki - vault write pki/root/generate/internal \ +vault write pki/root/generate/internal \ common_name=my-website.com \ ttl=8760h @@ -45,4 +45,17 @@ vault write pki/issue/example-dot-com \ vault read pki/crl/rotate +# make non-default second issuer +# help test getting multiple CRLs + vault write pki/root/generate/internal \ + common_name=mysecondwebsite.com \ + ttl=8760h \ + issuer_name=second + +vault write pki/roles/second-role \ + allowed_domains=mysecondwebsite.com \ + allow_subdomains=true \ + max_ttl=72h \ + issuer_ref=second + tail -f /dev/null From d14c42d57d2f8cb69154914e397075fb81bc189a Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 15:52:38 -0400 Subject: [PATCH 02/13] docs: add contributing instructions --- .gitignore | 4 ++++ README.md | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/.gitignore b/.gitignore index 4d648db..01db67a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ vault-pki-exporter # venom logs venom*log + +# envrc +.envrc + diff --git a/README.md b/README.md index 26be1bb..98f929b 100644 --- a/README.md +++ b/README.md @@ -89,3 +89,17 @@ level=error msg="failed to get certificate for pki/26:97:08:32:44:40:30:de:11:5z ``` Your batch size is probably too high. + +## Contributing + +### Testing + +Venom is used for tests, run `sudo venom run tests.yml` to perform integration tests. + +Unit tests would also most likely be welcome for contribution with go native tests. + +### Local Builds + +Simply run the docker compose setup - `sudo docker compose up --build`. + +You can navigate to the Vault UI locally at `http://localhost:8200` and use the root token value of `thisisatokenvalue` to login, as Vault is running in dev mode. It'll setup some initial settings for you with `vault-setup.sh.` From afc8991daf0e5770e4a92af11dce25f72ab7e1f1 Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 15:54:40 -0400 Subject: [PATCH 03/13] chore: delete un-used code --- pkg/vault-mon/prometheus.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pkg/vault-mon/prometheus.go b/pkg/vault-mon/prometheus.go index 269d2c4..c61c09d 100644 --- a/pkg/vault-mon/prometheus.go +++ b/pkg/vault-mon/prometheus.go @@ -25,13 +25,6 @@ var labelNames = []string{ "locality", } -type PromMetrics struct { - expiry *prometheus.GaugeVec - age *prometheus.GaugeVec - startdate *prometheus.GaugeVec - enddate *prometheus.GaugeVec -} - func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { expiry := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_cert_expiry", From c330b076693678fafaac7963980918108a81bd64 Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 16:02:54 -0400 Subject: [PATCH 04/13] fix: update to x509.RevocationList pkix.CertificateList is deprecated, switches to x509.RevocationList instead. Minimal methods need to change --- pkg/vault-mon/influx.go | 10 +++++----- pkg/vault-mon/pki.go | 11 +++++------ pkg/vault-mon/prometheus.go | 10 +++++----- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/pkg/vault-mon/influx.go b/pkg/vault-mon/influx.go index 0047239..ba82511 100644 --- a/pkg/vault-mon/influx.go +++ b/pkg/vault-mon/influx.go @@ -2,12 +2,12 @@ package vault_mon import ( "crypto/x509" - "crypto/x509/pkix" "fmt" - influx "github.com/influxdata/influxdb1-client" "os" "strings" "time" + + influx "github.com/influxdata/influxdb1-client" ) var hostname string @@ -72,7 +72,7 @@ func printCertificateInfluxPoint(pkiname string, cert *x509.Certificate) { fmt.Println(point.MarshalString()) } -func printCrlInfluxPoint(pkiname string, crl *pkix.CertificateList) { +func printCrlInfluxPoint(pkiname string, crl *x509.RevocationList) { now := time.Now() point := influx.Point{ Measurement: "x509_crl", @@ -81,8 +81,8 @@ func printCrlInfluxPoint(pkiname string, crl *pkix.CertificateList) { "source": pkiname, }, Fields: map[string]interface{}{ - "expiry": int(crl.TBSCertList.NextUpdate.Sub(now).Seconds()), - "nextupdate": int(crl.TBSCertList.NextUpdate.Unix()), + "expiry": int(crl.NextUpdate.Sub(now).Seconds()), + "nextupdate": int(crl.NextUpdate.Unix()), }, } fmt.Println(point.MarshalString()) diff --git a/pkg/vault-mon/pki.go b/pkg/vault-mon/pki.go index 4e34b2e..18a36cd 100644 --- a/pkg/vault-mon/pki.go +++ b/pkg/vault-mon/pki.go @@ -2,7 +2,6 @@ package vault_mon import ( "crypto/x509" - "crypto/x509/pkix" "encoding/pem" "fmt" "sync" @@ -20,7 +19,7 @@ import ( type PKI struct { path string certs map[string]*x509.Certificate - crls map[string]*pkix.CertificateList + crls map[string]*x509.RevocationList crlRawSize int expiredCertsCounter int vault *vaultapi.Client @@ -119,7 +118,7 @@ func (pki *PKI) loadCrl() error { } if pki.crls == nil { - pki.crls = make(map[string]*pkix.CertificateList) + pki.crls = make(map[string]*x509.RevocationList) log.Warningln("init an empty certs list") } // Step 2: Load CRL for each issuer @@ -171,7 +170,7 @@ func (pki *PKI) listIssuers() ([]string, error) { return issuers, nil } -func (pki *PKI) loadCrlForIssuer(issuerRef string) (*pkix.CertificateList, error) { +func (pki *PKI) loadCrlForIssuer(issuerRef string) (*x509.RevocationList, error) { secret, err := pki.vault.Logical().Read(fmt.Sprintf("/%s/issuer/%s/crl", pki.path, issuerRef)) if err != nil { return nil, fmt.Errorf("error finding CRL at /%s/issuer/%s/crl: %w", pki.path, issuerRef, err) @@ -200,7 +199,7 @@ func (pki *PKI) loadCrlForIssuer(issuerRef string) (*pkix.CertificateList, error pki.crlRawSize = len([]byte(crlData)) - crl, err := x509.ParseCRL(block.Bytes) + crl, err := x509.ParseRevocationList(block.Bytes) if err != nil { log.Errorf("Error parsing CRL for issuer %s: %v", issuerRef, err) return nil, fmt.Errorf("error parsing CRL for issuer %s: %w", issuerRef, err) @@ -327,7 +326,7 @@ func (pki *PKI) clearCerts() { pki.certsmux.Unlock() } -func (pki *PKI) GetCRLs() map[string]*pkix.CertificateList { +func (pki *PKI) GetCRLs() map[string]*x509.RevocationList { pki.crlmux.Lock() defer pki.crlmux.Unlock() return pki.crls diff --git a/pkg/vault-mon/prometheus.go b/pkg/vault-mon/prometheus.go index c61c09d..2571db6 100644 --- a/pkg/vault-mon/prometheus.go +++ b/pkg/vault-mon/prometheus.go @@ -76,14 +76,14 @@ func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { for _, crl := range pki.GetCRLs() { if crl != nil { // issuer string is vanity, such as CN=my-website.com - issuer := crl.TBSCertList.Issuer.String() + issuer := crl.Issuer.String() - crl_expiry.WithLabelValues(pkiname, issuer).Set(float64(crl.TBSCertList.NextUpdate.Sub(now).Seconds())) - crl_nextupdate.WithLabelValues(pkiname, issuer).Set(float64(crl.TBSCertList.NextUpdate.Unix())) - crl_length.WithLabelValues(pkiname, issuer).Set(float64(len(crl.TBSCertList.RevokedCertificates))) + crl_expiry.WithLabelValues(pkiname, issuer).Set(float64(crl.NextUpdate.Sub(now).Seconds())) + crl_nextupdate.WithLabelValues(pkiname, issuer).Set(float64(crl.NextUpdate.Unix())) + crl_length.WithLabelValues(pkiname, issuer).Set(float64(len(crl.RevokedCertificates))) crl_byte_size.WithLabelValues(pkiname, issuer).Set(float64(pki.crlRawSize)) // gather revoked certs from the CRL so we can exclude their metrics later - for _, revokedCert := range crl.TBSCertList.RevokedCertificates { + for _, revokedCert := range crl.RevokedCertificates { revokedCerts[revokedCert.SerialNumber.String()] = struct{}{} } } From 5514eb0cdbc31dbba042de293a7ce96d411041e5 Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 16:06:33 -0400 Subject: [PATCH 05/13] chore: clean up debug logging --- pkg/vault-mon/pki.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/vault-mon/pki.go b/pkg/vault-mon/pki.go index 18a36cd..c16777e 100644 --- a/pkg/vault-mon/pki.go +++ b/pkg/vault-mon/pki.go @@ -181,16 +181,12 @@ func (pki *PKI) loadCrlForIssuer(issuerRef string) (*x509.RevocationList, error) return nil, fmt.Errorf("no secret found for issuer %s", issuerRef) } - log.Infof("Issuer: %s, Secret Data: %v", issuerRef, secret.Data) - crlData, ok := secret.Data["crl"].(string) if !ok || crlData == "" { log.Errorf("CRL data missing or invalid for issuer %s", issuerRef) return nil, fmt.Errorf("crl data missing or invalid for issuer %s", issuerRef) } - log.Infof("Issuer: %s, Raw CRL Data: %s", issuerRef, crlData) - block, _ := pem.Decode([]byte(crlData)) if block == nil { log.Errorf("Failed to parse PEM block for issuer %s. Raw PEM data: %s", issuerRef, crlData) @@ -205,7 +201,7 @@ func (pki *PKI) loadCrlForIssuer(issuerRef string) (*x509.RevocationList, error) return nil, fmt.Errorf("error parsing CRL for issuer %s: %w", issuerRef, err) } - log.Infof("Successfully loaded CRL for issuer %s", issuerRef) + log.Debugf("Successfully loaded CRL for issuer %s", issuerRef) return crl, nil } From feccbddb2b31e1befaddc10fdd10ec765f397881 Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 16:13:17 -0400 Subject: [PATCH 06/13] feat!: support CRLs with multiple issuers Resolves: https://github.com/aarnaud/vault-pki-exporter/issues/19 It is common practice to have multiple issuers and therefore CRLs in a single Vault PKI secrets engine. Previously we'd only have metrics for the default issuer in a PKI secrets engine. This refactors metrics gathering to support CRL related metrics for all issuers in a secrets engine. This is marked as a breaking change due to the *addition* of an `issuer` label to metrics like `x509_crl_length` but in most cases adding a new label to existing metrics won't break alerting or recording rules compared to removing a label. Regardless proper notice is given. Old metric example: ``` x509_crl_nextupdate{source="pki/"} 1.730633058e+09 ``` New metric example: ``` x509_crl_nextupdate{issuer="CN=my-website.com",source="pki/"} 1.730633058e+09 x509_crl_nextupdate{issuer="CN=mysecondwebsite.com",source="pki/"} 1.730633058e+09 ``` --- .envrc | 4 ++ README.md | 4 +- pkg/vault-mon/influx.go | 6 ++- pkg/vault-mon/pki.go | 101 ++++++++++++++++++++++++++++++------ pkg/vault-mon/prometheus.go | 29 ++++++----- pkg/vault/client.go | 4 +- tests.yml | 4 +- vault-setup.sh | 15 +++++- 8 files changed, 132 insertions(+), 35 deletions(-) create mode 100644 .envrc diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..fc78058 --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +#!/bin/sh + +export VAULT_ADDR="http://localhost:8200" +export VAULT_TOKEN="thisisatokenvalue" diff --git a/README.md b/README.md index 147f0e9..26be1bb 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,10 @@ x509_cert,common_name=My\ PKI\ CA,country=CA,host=your.hostname.com,locality=Mon ```console # HELP x509_crl_expiry # TYPE x509_crl_expiry gauge -x509_crl_expiry{source="pki-test/"} 243687.999819847 +x509_crl_expiry{source="pki-test/", issuer="CN=example.com"} 243687.999819847 # HELP x509_crl_nextupdate # TYPE x509_crl_nextupdate gauge -x509_crl_nextupdate{source="pki-test/"} 1.573235993e+09 +x509_crl_nextupdate{source="pki-test/", issuer="CN=example.com"} 1.573235993e+09 # HELP x509_cert_age # TYPE x509_cert_age gauge x509_cert_age{common_name="My PKI CA",country="CA",locality="Montreal",organization="Example",organizational_unit="WebService",province="QC",serial="0e-50-38-4d-18-69-52-54-1d-71-31-49-1b-a8-06-c7-4f-23-64-26",source="pki-test/"} 15543.000180153 diff --git a/pkg/vault-mon/influx.go b/pkg/vault-mon/influx.go index 59baa9f..0047239 100644 --- a/pkg/vault-mon/influx.go +++ b/pkg/vault-mon/influx.go @@ -36,8 +36,10 @@ func InfluxWatchCerts(pkimon *PKIMon, interval time.Duration, loop bool) { func influxProcessData(pkimon *PKIMon) { for pkiname, pki := range pkimon.GetPKIs() { - if crl := pki.GetCRL(); crl != nil { - printCrlInfluxPoint(pkiname, crl) + for _, crl := range pki.GetCRLs() { + if crl != nil { + printCrlInfluxPoint(pkiname, crl) + } } for _, cert := range pki.GetCerts() { printCertificateInfluxPoint(pkiname, cert) diff --git a/pkg/vault-mon/pki.go b/pkg/vault-mon/pki.go index 5e0dd04..f2228b7 100644 --- a/pkg/vault-mon/pki.go +++ b/pkg/vault-mon/pki.go @@ -20,7 +20,7 @@ import ( type PKI struct { path string certs map[string]*x509.Certificate - crl *pkix.CertificateList + crls map[string]*pkix.CertificateList crlRawSize int expiredCertsCounter int vault *vaultapi.Client @@ -111,31 +111,102 @@ func (mon *PKIMon) GetPKIs() map[string]*PKI { func (pki *PKI) loadCrl() error { pki.crlmux.Lock() defer pki.crlmux.Unlock() - secret, err := pki.vault.Logical().Read(fmt.Sprintf("%scert/crl", pki.path)) + + // List all issuers to get multiple CRLs per PKI engine + issuers, err := pki.listIssuers() if err != nil { return err } - // avoids a segfault + if pki.crls == nil { + pki.crls = make(map[string]*pkix.CertificateList) + log.Warningln("CRLs nil for issuers") + } + + for _, issuerRef := range issuers { + crl, err := pki.loadCrlForIssuer(issuerRef) + if err != nil || crl == nil { + log.Errorf("loadCrl() failed to load CRL for issuer %s, error: %v", issuerRef, err) + } else { + pki.crls[issuerRef] = crl + } + } + + return nil +} + +func (pki *PKI) listIssuers() ([]string, error) { + // Define the path where the issuers are listed in your Vault setup + // This path might be different based on your Vault configuration + + // Make a read request to Vault + secret, err := pki.vault.Logical().List(fmt.Sprintf("%s/issuers", pki.path)) + if err != nil { + return nil, fmt.Errorf("error listing issuers: %w", err) + } + if secret == nil || secret.Data == nil { - return nil + return []string{}, nil } - secretCert := vault.SecretCertificate{} - err = mapstructure.Decode(secret.Data, &secretCert) + // Extract the list of issuers from the response + // The key under which issuers Ware listed might vary, so adjust "keys" accordingly + issuerRefs, ok := secret.Data["keys"].([]interface{}) + if !ok { + return nil, fmt.Errorf("failed to parse issuer list") + } + + // Convert issuer references to a slice of strings + issuers := make([]string, len(issuerRefs)) + for i, ref := range issuerRefs { + issuer, ok := ref.(string) + if !ok { + return nil, fmt.Errorf("invalid issuer reference type") + } + issuers[i] = issuer + } + + return issuers, nil +} + +func (pki *PKI) loadCrlForIssuer(issuerRef string) (*pkix.CertificateList, error) { + secret, err := pki.vault.Logical().Read(fmt.Sprintf("/%s/issuer/%s/crl", pki.path, issuerRef)) if err != nil { - return err + return nil, fmt.Errorf("error finding CRL at /%s/issuer/%s/crl: %w", pki.path, issuerRef, err) + } + + if secret == nil { + log.Errorf("No secret found for issuer %s", issuerRef) + return nil, fmt.Errorf("no secret found for issuer %s", issuerRef) + } + + log.Infof("Issuer: %s, Secret Data: %v", issuerRef, secret.Data) + + crlData, ok := secret.Data["crl"].(string) + if !ok || crlData == "" { + log.Errorf("CRL data missing or invalid for issuer %s", issuerRef) + return nil, fmt.Errorf("crl data missing or invalid for issuer %s", issuerRef) } - block, _ := pem.Decode([]byte([]byte(secretCert.Certificate))) - pki.crlRawSize = len([]byte(secretCert.Certificate)) + + log.Infof("Issuer: %s, Raw CRL Data: %s", issuerRef, crlData) + + block, _ := pem.Decode([]byte(crlData)) + if block == nil { + log.Errorf("Failed to parse PEM block for issuer %s. Raw PEM data: %s", issuerRef, crlData) + return nil, fmt.Errorf("failed to parse PEM block for issuer %s", issuerRef) + } + + pki.crlRawSize = len([]byte(crlData)) + crl, err := x509.ParseCRL(block.Bytes) if err != nil { - log.Errorf("failed to load CRL for %s, error: %w", pki.path, err) - return err + log.Errorf("Error parsing CRL for issuer %s: %v", issuerRef, err) + return nil, fmt.Errorf("error parsing CRL for issuer %s: %w", issuerRef, err) } - pki.crl = crl - return nil + log.Infof("Successfully loaded CRL for issuer %s", issuerRef) + + return crl, nil } func (pki *PKI) loadCerts() error { @@ -254,10 +325,10 @@ func (pki *PKI) clearCerts() { pki.certsmux.Unlock() } -func (pki *PKI) GetCRL() *pkix.CertificateList { +func (pki *PKI) GetCRLs() map[string]*pkix.CertificateList { pki.crlmux.Lock() defer pki.crlmux.Unlock() - return pki.crl + return pki.crls } func (pki *PKI) GetCerts() map[string]*x509.Certificate { diff --git a/pkg/vault-mon/prometheus.go b/pkg/vault-mon/prometheus.go index 051f737..269d2c4 100644 --- a/pkg/vault-mon/prometheus.go +++ b/pkg/vault-mon/prometheus.go @@ -55,18 +55,18 @@ func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { }, []string{"source"}) crl_expiry := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_crl_expiry", - }, []string{"source"}) + }, []string{"source", "issuer"}) crl_nextupdate := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_crl_nextupdate", - }, []string{"source"}) + }, []string{"source", "issuer"}) crl_byte_size := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_crl_byte_size", Help: "Size of raw certificate revocation list pem stored in vault", - }, []string{"source"}) + }, []string{"source", "issuer"}) crl_length := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_crl_length", Help: "Length of certificate revocation list", - }, []string{"source"}) + }, []string{"source", "issuer"}) promWatchCertsDuration := promauto.NewHistogram(prometheus.HistogramOpts{ Name: "x509_watch_certs_duration_seconds", Help: "Duration of promWatchCerts execution", @@ -80,14 +80,19 @@ func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { revokedCerts := make(map[string]struct{}) for pkiname, pki := range pkis { - if crl := pki.GetCRL(); crl != nil { - crl_expiry.WithLabelValues(pkiname).Set(float64(crl.TBSCertList.NextUpdate.Sub(now).Seconds())) - crl_nextupdate.WithLabelValues(pkiname).Set(float64(crl.TBSCertList.NextUpdate.Unix())) - crl_length.WithLabelValues(pkiname).Set(float64(len(crl.TBSCertList.RevokedCertificates))) - crl_byte_size.WithLabelValues(pkiname).Set(float64(pki.crlRawSize)) - // gather revoked certs from the CRL so we can exclude their metrics later - for _, revokedCert := range crl.TBSCertList.RevokedCertificates { - revokedCerts[revokedCert.SerialNumber.String()] = struct{}{} + for _, crl := range pki.GetCRLs() { + if crl != nil { + // issuer string is vanity, such as CN=my-website.com + issuer := crl.TBSCertList.Issuer.String() + + crl_expiry.WithLabelValues(pkiname, issuer).Set(float64(crl.TBSCertList.NextUpdate.Sub(now).Seconds())) + crl_nextupdate.WithLabelValues(pkiname, issuer).Set(float64(crl.TBSCertList.NextUpdate.Unix())) + crl_length.WithLabelValues(pkiname, issuer).Set(float64(len(crl.TBSCertList.RevokedCertificates))) + crl_byte_size.WithLabelValues(pkiname, issuer).Set(float64(pki.crlRawSize)) + // gather revoked certs from the CRL so we can exclude their metrics later + for _, revokedCert := range crl.TBSCertList.RevokedCertificates { + revokedCerts[revokedCert.SerialNumber.String()] = struct{}{} + } } } for _, cert := range pki.GetCerts() { diff --git a/pkg/vault/client.go b/pkg/vault/client.go index ce8a033..b1136cf 100644 --- a/pkg/vault/client.go +++ b/pkg/vault/client.go @@ -116,11 +116,11 @@ func (vault *ClientWrapper) GetSecret(path string, fn secretCallback) error { time.Sleep(time.Duration(secret.LeaseDuration) * time.Second) secret, err = vault.Client.Logical().Read(path) if err != nil { - log.Errorln("[vault]", err) + log.Error("[vault]", err) continue } if secret == nil { - log.Errorln("[vault] secret not found : %s", path) + log.Errorf("[vault] secret not found : %s", path) continue } fn(secret) diff --git a/tests.yml b/tests.yml index fa2ecd6..37ff3d8 100644 --- a/tests.yml +++ b/tests.yml @@ -323,7 +323,9 @@ testcases: - result.body ShouldContainSubstring common_name="*.second-ca.example.com" - result.body ShouldContainSubstring common_name="alt.second-ca.example.com" - result.body ShouldNotContainSubstring common_name="revokeme.first-ca.example.com" - - result.body ShouldContainSubstring x509_crl_length{source="first-ca/"} 1 + # CRLs for each issuer + - result.body ShouldContainSubstring x509_crl_length{issuer="CN=First CA,OU=WebService,O=example,L=Montreal,ST=QC,C=CA",source="first-ca/"} 1 + - result.body ShouldContainSubstring x509_crl_length{issuer="CN=Second CA,OU=VPN,O=example2,L=Montreal,ST=QC,C=CA",source="second-ca/"} 0 - name: docker compose down steps: diff --git a/vault-setup.sh b/vault-setup.sh index c654ff5..bc748e0 100755 --- a/vault-setup.sh +++ b/vault-setup.sh @@ -18,7 +18,7 @@ vault secrets enable pki vault secrets tune -max-lease-ttl=87600h pki - vault write pki/root/generate/internal \ +vault write pki/root/generate/internal \ common_name=my-website.com \ ttl=8760h @@ -45,4 +45,17 @@ vault write pki/issue/example-dot-com \ vault read pki/crl/rotate +# make non-default second issuer +# help test getting multiple CRLs + vault write pki/root/generate/internal \ + common_name=mysecondwebsite.com \ + ttl=8760h \ + issuer_name=second + +vault write pki/roles/second-role \ + allowed_domains=mysecondwebsite.com \ + allow_subdomains=true \ + max_ttl=72h \ + issuer_ref=second + tail -f /dev/null From a86e0a2aba701b5ad552a366377cd2fa61ebc180 Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 15:52:38 -0400 Subject: [PATCH 07/13] docs: add contributing instructions --- .gitignore | 4 ++++ README.md | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/.gitignore b/.gitignore index 4d648db..01db67a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ vault-pki-exporter # venom logs venom*log + +# envrc +.envrc + diff --git a/README.md b/README.md index 26be1bb..98f929b 100644 --- a/README.md +++ b/README.md @@ -89,3 +89,17 @@ level=error msg="failed to get certificate for pki/26:97:08:32:44:40:30:de:11:5z ``` Your batch size is probably too high. + +## Contributing + +### Testing + +Venom is used for tests, run `sudo venom run tests.yml` to perform integration tests. + +Unit tests would also most likely be welcome for contribution with go native tests. + +### Local Builds + +Simply run the docker compose setup - `sudo docker compose up --build`. + +You can navigate to the Vault UI locally at `http://localhost:8200` and use the root token value of `thisisatokenvalue` to login, as Vault is running in dev mode. It'll setup some initial settings for you with `vault-setup.sh.` From 269846d4d11e654c206e076aaa540b802ebab9bf Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 15:54:40 -0400 Subject: [PATCH 08/13] chore: delete un-used code --- pkg/vault-mon/prometheus.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pkg/vault-mon/prometheus.go b/pkg/vault-mon/prometheus.go index 269d2c4..c61c09d 100644 --- a/pkg/vault-mon/prometheus.go +++ b/pkg/vault-mon/prometheus.go @@ -25,13 +25,6 @@ var labelNames = []string{ "locality", } -type PromMetrics struct { - expiry *prometheus.GaugeVec - age *prometheus.GaugeVec - startdate *prometheus.GaugeVec - enddate *prometheus.GaugeVec -} - func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { expiry := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_cert_expiry", From fd5567f37bfb17e17f7d023c9b56916514004782 Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 16:13:50 -0400 Subject: [PATCH 09/13] fix: update to x509.RevocationList pkix.CertificateList is deprecated, switches to x509.RevocationList instead. Minimal methods need to change --- pkg/vault-mon/influx.go | 10 +++++----- pkg/vault-mon/pki.go | 13 ++++++------- pkg/vault-mon/prometheus.go | 10 +++++----- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/pkg/vault-mon/influx.go b/pkg/vault-mon/influx.go index 0047239..ba82511 100644 --- a/pkg/vault-mon/influx.go +++ b/pkg/vault-mon/influx.go @@ -2,12 +2,12 @@ package vault_mon import ( "crypto/x509" - "crypto/x509/pkix" "fmt" - influx "github.com/influxdata/influxdb1-client" "os" "strings" "time" + + influx "github.com/influxdata/influxdb1-client" ) var hostname string @@ -72,7 +72,7 @@ func printCertificateInfluxPoint(pkiname string, cert *x509.Certificate) { fmt.Println(point.MarshalString()) } -func printCrlInfluxPoint(pkiname string, crl *pkix.CertificateList) { +func printCrlInfluxPoint(pkiname string, crl *x509.RevocationList) { now := time.Now() point := influx.Point{ Measurement: "x509_crl", @@ -81,8 +81,8 @@ func printCrlInfluxPoint(pkiname string, crl *pkix.CertificateList) { "source": pkiname, }, Fields: map[string]interface{}{ - "expiry": int(crl.TBSCertList.NextUpdate.Sub(now).Seconds()), - "nextupdate": int(crl.TBSCertList.NextUpdate.Unix()), + "expiry": int(crl.NextUpdate.Sub(now).Seconds()), + "nextupdate": int(crl.NextUpdate.Unix()), }, } fmt.Println(point.MarshalString()) diff --git a/pkg/vault-mon/pki.go b/pkg/vault-mon/pki.go index f2228b7..8c9c9a0 100644 --- a/pkg/vault-mon/pki.go +++ b/pkg/vault-mon/pki.go @@ -2,7 +2,6 @@ package vault_mon import ( "crypto/x509" - "crypto/x509/pkix" "encoding/pem" "fmt" "sync" @@ -20,7 +19,7 @@ import ( type PKI struct { path string certs map[string]*x509.Certificate - crls map[string]*pkix.CertificateList + crls map[string]*x509.RevocationList crlRawSize int expiredCertsCounter int vault *vaultapi.Client @@ -119,8 +118,8 @@ func (pki *PKI) loadCrl() error { } if pki.crls == nil { - pki.crls = make(map[string]*pkix.CertificateList) - log.Warningln("CRLs nil for issuers") + pki.crls = make(map[string]*x509.RevocationList) + log.Warningln("init an empty certs list") } for _, issuerRef := range issuers { @@ -169,7 +168,7 @@ func (pki *PKI) listIssuers() ([]string, error) { return issuers, nil } -func (pki *PKI) loadCrlForIssuer(issuerRef string) (*pkix.CertificateList, error) { +func (pki *PKI) loadCrlForIssuer(issuerRef string) (*x509.RevocationList, error) { secret, err := pki.vault.Logical().Read(fmt.Sprintf("/%s/issuer/%s/crl", pki.path, issuerRef)) if err != nil { return nil, fmt.Errorf("error finding CRL at /%s/issuer/%s/crl: %w", pki.path, issuerRef, err) @@ -198,7 +197,7 @@ func (pki *PKI) loadCrlForIssuer(issuerRef string) (*pkix.CertificateList, error pki.crlRawSize = len([]byte(crlData)) - crl, err := x509.ParseCRL(block.Bytes) + crl, err := x509.ParseRevocationList(block.Bytes) if err != nil { log.Errorf("Error parsing CRL for issuer %s: %v", issuerRef, err) return nil, fmt.Errorf("error parsing CRL for issuer %s: %w", issuerRef, err) @@ -325,7 +324,7 @@ func (pki *PKI) clearCerts() { pki.certsmux.Unlock() } -func (pki *PKI) GetCRLs() map[string]*pkix.CertificateList { +func (pki *PKI) GetCRLs() map[string]*x509.RevocationList { pki.crlmux.Lock() defer pki.crlmux.Unlock() return pki.crls diff --git a/pkg/vault-mon/prometheus.go b/pkg/vault-mon/prometheus.go index c61c09d..2571db6 100644 --- a/pkg/vault-mon/prometheus.go +++ b/pkg/vault-mon/prometheus.go @@ -76,14 +76,14 @@ func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { for _, crl := range pki.GetCRLs() { if crl != nil { // issuer string is vanity, such as CN=my-website.com - issuer := crl.TBSCertList.Issuer.String() + issuer := crl.Issuer.String() - crl_expiry.WithLabelValues(pkiname, issuer).Set(float64(crl.TBSCertList.NextUpdate.Sub(now).Seconds())) - crl_nextupdate.WithLabelValues(pkiname, issuer).Set(float64(crl.TBSCertList.NextUpdate.Unix())) - crl_length.WithLabelValues(pkiname, issuer).Set(float64(len(crl.TBSCertList.RevokedCertificates))) + crl_expiry.WithLabelValues(pkiname, issuer).Set(float64(crl.NextUpdate.Sub(now).Seconds())) + crl_nextupdate.WithLabelValues(pkiname, issuer).Set(float64(crl.NextUpdate.Unix())) + crl_length.WithLabelValues(pkiname, issuer).Set(float64(len(crl.RevokedCertificates))) crl_byte_size.WithLabelValues(pkiname, issuer).Set(float64(pki.crlRawSize)) // gather revoked certs from the CRL so we can exclude their metrics later - for _, revokedCert := range crl.TBSCertList.RevokedCertificates { + for _, revokedCert := range crl.RevokedCertificates { revokedCerts[revokedCert.SerialNumber.String()] = struct{}{} } } From 2734737c676d99be33874a964416037040e1c9f8 Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 16:06:33 -0400 Subject: [PATCH 10/13] chore: clean up debug logging --- pkg/vault-mon/pki.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/vault-mon/pki.go b/pkg/vault-mon/pki.go index 8c9c9a0..a688a3d 100644 --- a/pkg/vault-mon/pki.go +++ b/pkg/vault-mon/pki.go @@ -179,16 +179,12 @@ func (pki *PKI) loadCrlForIssuer(issuerRef string) (*x509.RevocationList, error) return nil, fmt.Errorf("no secret found for issuer %s", issuerRef) } - log.Infof("Issuer: %s, Secret Data: %v", issuerRef, secret.Data) - crlData, ok := secret.Data["crl"].(string) if !ok || crlData == "" { log.Errorf("CRL data missing or invalid for issuer %s", issuerRef) return nil, fmt.Errorf("crl data missing or invalid for issuer %s", issuerRef) } - log.Infof("Issuer: %s, Raw CRL Data: %s", issuerRef, crlData) - block, _ := pem.Decode([]byte(crlData)) if block == nil { log.Errorf("Failed to parse PEM block for issuer %s. Raw PEM data: %s", issuerRef, crlData) @@ -203,7 +199,7 @@ func (pki *PKI) loadCrlForIssuer(issuerRef string) (*x509.RevocationList, error) return nil, fmt.Errorf("error parsing CRL for issuer %s: %w", issuerRef, err) } - log.Infof("Successfully loaded CRL for issuer %s", issuerRef) + log.Debugf("Successfully loaded CRL for issuer %s", issuerRef) return crl, nil } From b6d17bfcc93795eaee6108cac906feef3b1f3594 Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 16:20:57 -0400 Subject: [PATCH 11/13] chore: clean up error logging --- pkg/vault-mon/pki.go | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/pkg/vault-mon/pki.go b/pkg/vault-mon/pki.go index a688a3d..5aaf377 100644 --- a/pkg/vault-mon/pki.go +++ b/pkg/vault-mon/pki.go @@ -124,8 +124,10 @@ func (pki *PKI) loadCrl() error { for _, issuerRef := range issuers { crl, err := pki.loadCrlForIssuer(issuerRef) - if err != nil || crl == nil { + if err != nil { log.Errorf("loadCrl() failed to load CRL for issuer %s, error: %v", issuerRef, err) + } else if crl == nil { + log.Errorf("CRL cannot be loaded for issuer %s", issuerRef) } else { pki.crls[issuerRef] = crl } @@ -135,10 +137,8 @@ func (pki *PKI) loadCrl() error { } func (pki *PKI) listIssuers() ([]string, error) { - // Define the path where the issuers are listed in your Vault setup - // This path might be different based on your Vault configuration - // Make a read request to Vault + // Request PKI engine Vault issuers secret, err := pki.vault.Logical().List(fmt.Sprintf("%s/issuers", pki.path)) if err != nil { return nil, fmt.Errorf("error listing issuers: %w", err) @@ -148,14 +148,12 @@ func (pki *PKI) listIssuers() ([]string, error) { return []string{}, nil } - // Extract the list of issuers from the response - // The key under which issuers Ware listed might vary, so adjust "keys" accordingly + // The key under which issuers are listed might vary, so adjust "keys" accordingly issuerRefs, ok := secret.Data["keys"].([]interface{}) if !ok { return nil, fmt.Errorf("failed to parse issuer list") } - // Convert issuer references to a slice of strings issuers := make([]string, len(issuerRefs)) for i, ref := range issuerRefs { issuer, ok := ref.(string) @@ -175,19 +173,16 @@ func (pki *PKI) loadCrlForIssuer(issuerRef string) (*x509.RevocationList, error) } if secret == nil { - log.Errorf("No secret found for issuer %s", issuerRef) return nil, fmt.Errorf("no secret found for issuer %s", issuerRef) } crlData, ok := secret.Data["crl"].(string) if !ok || crlData == "" { - log.Errorf("CRL data missing or invalid for issuer %s", issuerRef) return nil, fmt.Errorf("crl data missing or invalid for issuer %s", issuerRef) } block, _ := pem.Decode([]byte(crlData)) if block == nil { - log.Errorf("Failed to parse PEM block for issuer %s. Raw PEM data: %s", issuerRef, crlData) return nil, fmt.Errorf("failed to parse PEM block for issuer %s", issuerRef) } @@ -195,7 +190,6 @@ func (pki *PKI) loadCrlForIssuer(issuerRef string) (*x509.RevocationList, error) crl, err := x509.ParseRevocationList(block.Bytes) if err != nil { - log.Errorf("Error parsing CRL for issuer %s: %v", issuerRef, err) return nil, fmt.Errorf("error parsing CRL for issuer %s: %w", issuerRef, err) } From 27f113d5003bd6261cdce9a82345f5e28f77d7bc Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 16:33:22 -0400 Subject: [PATCH 12/13] ref: switch to just CN for issuer string prettier to just use CN --- README.md | 4 ++-- pkg/vault-mon/prometheus.go | 2 +- tests.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 98f929b..36ec2ca 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,10 @@ x509_cert,common_name=My\ PKI\ CA,country=CA,host=your.hostname.com,locality=Mon ```console # HELP x509_crl_expiry # TYPE x509_crl_expiry gauge -x509_crl_expiry{source="pki-test/", issuer="CN=example.com"} 243687.999819847 +x509_crl_expiry{source="pki-test/", issuer="example.com"} 243687.999819847 # HELP x509_crl_nextupdate # TYPE x509_crl_nextupdate gauge -x509_crl_nextupdate{source="pki-test/", issuer="CN=example.com"} 1.573235993e+09 +x509_crl_nextupdate{source="pki-test/", issuer="example.com"} 1.573235993e+09 # HELP x509_cert_age # TYPE x509_cert_age gauge x509_cert_age{common_name="My PKI CA",country="CA",locality="Montreal",organization="Example",organizational_unit="WebService",province="QC",serial="0e-50-38-4d-18-69-52-54-1d-71-31-49-1b-a8-06-c7-4f-23-64-26",source="pki-test/"} 15543.000180153 diff --git a/pkg/vault-mon/prometheus.go b/pkg/vault-mon/prometheus.go index 2571db6..a560171 100644 --- a/pkg/vault-mon/prometheus.go +++ b/pkg/vault-mon/prometheus.go @@ -76,7 +76,7 @@ func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { for _, crl := range pki.GetCRLs() { if crl != nil { // issuer string is vanity, such as CN=my-website.com - issuer := crl.Issuer.String() + issuer := crl.Issuer.CommonName crl_expiry.WithLabelValues(pkiname, issuer).Set(float64(crl.NextUpdate.Sub(now).Seconds())) crl_nextupdate.WithLabelValues(pkiname, issuer).Set(float64(crl.NextUpdate.Unix())) diff --git a/tests.yml b/tests.yml index 37ff3d8..c5bf0af 100644 --- a/tests.yml +++ b/tests.yml @@ -324,8 +324,8 @@ testcases: - result.body ShouldContainSubstring common_name="alt.second-ca.example.com" - result.body ShouldNotContainSubstring common_name="revokeme.first-ca.example.com" # CRLs for each issuer - - result.body ShouldContainSubstring x509_crl_length{issuer="CN=First CA,OU=WebService,O=example,L=Montreal,ST=QC,C=CA",source="first-ca/"} 1 - - result.body ShouldContainSubstring x509_crl_length{issuer="CN=Second CA,OU=VPN,O=example2,L=Montreal,ST=QC,C=CA",source="second-ca/"} 0 + - result.body ShouldContainSubstring x509_crl_length{issuer="my-website.com",source="pki/"} 1 + - result.body ShouldContainSubstring x509_crl_length{issuer="mysecondwebsite.com",source="pki/"} 0 - name: docker compose down steps: From a70fbaeed9e5e17e679fa1b8e610feaf43066bcc Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Thu, 17 Oct 2024 16:45:08 -0400 Subject: [PATCH 13/13] chore: remove old comment --- pkg/vault-mon/prometheus.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/vault-mon/prometheus.go b/pkg/vault-mon/prometheus.go index a560171..5c441b4 100644 --- a/pkg/vault-mon/prometheus.go +++ b/pkg/vault-mon/prometheus.go @@ -75,7 +75,6 @@ func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { for pkiname, pki := range pkis { for _, crl := range pki.GetCRLs() { if crl != nil { - // issuer string is vanity, such as CN=my-website.com issuer := crl.Issuer.CommonName crl_expiry.WithLabelValues(pkiname, issuer).Set(float64(crl.NextUpdate.Sub(now).Seconds()))