Skip to content

Commit

Permalink
Merge pull request aarnaud#26 from wbollock/ref/multiple_issuer_crls
Browse files Browse the repository at this point in the history
feat!: support CRLs with multiple issuers
  • Loading branch information
wbollock authored Nov 14, 2024
2 parents 1fc17a7 + a70fbae commit 10c22e2
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 49 deletions.
4 changes: 4 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh

export VAULT_ADDR="http://localhost:8200"
export VAULT_TOKEN="thisisatokenvalue"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ vault-pki-exporter

# venom logs
venom*log

# envrc
.envrc

18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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="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="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
Expand All @@ -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.`
16 changes: 9 additions & 7 deletions pkg/vault-mon/influx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -70,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",
Expand All @@ -79,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())
Expand Down
94 changes: 77 additions & 17 deletions pkg/vault-mon/pki.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package vault_mon

import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"sync"
Expand All @@ -20,7 +19,7 @@ import (
type PKI struct {
path string
certs map[string]*x509.Certificate
crl *pkix.CertificateList
crls map[string]*x509.RevocationList
crlRawSize int
expiredCertsCounter int
vault *vaultapi.Client
Expand Down Expand Up @@ -111,31 +110,92 @@ 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]*x509.RevocationList)
log.Warningln("init an empty certs list")
}

for _, issuerRef := range issuers {
crl, err := pki.loadCrlForIssuer(issuerRef)
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
}
}

return nil
}

func (pki *PKI) listIssuers() ([]string, error) {

// 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)
}

if secret == nil || secret.Data == nil {
return nil
return []string{}, nil
}

// 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")
}

secretCert := vault.SecretCertificate{}
err = mapstructure.Decode(secret.Data, &secretCert)
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) (*x509.RevocationList, 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 {
return nil, fmt.Errorf("no secret found for issuer %s", issuerRef)
}
block, _ := pem.Decode([]byte([]byte(secretCert.Certificate)))
pki.crlRawSize = len([]byte(secretCert.Certificate))
crl, err := x509.ParseCRL(block.Bytes)

crlData, ok := secret.Data["crl"].(string)
if !ok || crlData == "" {
return nil, fmt.Errorf("crl data missing or invalid for issuer %s", issuerRef)
}

block, _ := pem.Decode([]byte(crlData))
if block == nil {
return nil, fmt.Errorf("failed to parse PEM block for issuer %s", issuerRef)
}

pki.crlRawSize = len([]byte(crlData))

crl, err := x509.ParseRevocationList(block.Bytes)
if err != nil {
log.Errorf("failed to load CRL for %s, error: %w", pki.path, err)
return err
return nil, fmt.Errorf("error parsing CRL for issuer %s: %w", issuerRef, err)
}
pki.crl = crl

return nil
log.Debugf("Successfully loaded CRL for issuer %s", issuerRef)

return crl, nil
}

func (pki *PKI) loadCerts() error {
Expand Down Expand Up @@ -254,10 +314,10 @@ func (pki *PKI) clearCerts() {
pki.certsmux.Unlock()
}

func (pki *PKI) GetCRL() *pkix.CertificateList {
func (pki *PKI) GetCRLs() map[string]*x509.RevocationList {
pki.crlmux.Lock()
defer pki.crlmux.Unlock()
return pki.crl
return pki.crls
}

func (pki *PKI) GetCerts() map[string]*x509.Certificate {
Expand Down
35 changes: 16 additions & 19 deletions pkg/vault-mon/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -55,18 +48,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",
Expand All @@ -80,14 +73,18 @@ 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 := 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()))
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.RevokedCertificates {
revokedCerts[revokedCert.SerialNumber.String()] = struct{}{}
}
}
}
for _, cert := range pki.GetCerts() {
Expand Down
4 changes: 2 additions & 2 deletions pkg/vault/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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="my-website.com",source="pki/"} 1
- result.body ShouldContainSubstring x509_crl_length{issuer="mysecondwebsite.com",source="pki/"} 0

- name: docker compose down
steps:
Expand Down
15 changes: 14 additions & 1 deletion vault-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

0 comments on commit 10c22e2

Please sign in to comment.