diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml new file mode 100644 index 0000000..f3be471 --- /dev/null +++ b/.github/workflows/golangci-lint.yaml @@ -0,0 +1,23 @@ +name: golangci-lint +on: + push: + tags: + - v* + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.21 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..27d02ad --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,43 @@ +repos: +- repo: https://github.com/tekwizely/pre-commit-golang + rev: v1.0.0-rc.1 + hooks: + # + # Go Build + # + - id: go-build-mod + - id: go-build-repo-mod + # + # Go Mod Tidy + # + - id: go-mod-tidy + - id: go-mod-tidy-repo + # + # Go Test + # + - id: go-test-mod + - id: go-test-repo-mod + # + # Formatters + # + - id: go-fmt + # + # + # + # Style Checkers + # + - id: go-lint + # + # GolangCI-Lint + # local hook from https://github.com/golangci/golangci-lin> + # other versions are outdated +- repo: local + hooks: + - id: golangci-lint + name: golangci-lint + description: Fast linters runner for Go. Note that only > + entry: golangci-lint run --new-from-rev HEAD --fix + types: [go] + language: golang + require_serial: true + pass_filenames: false diff --git a/README.md b/README.md index a98a29c..19ce89d 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,8 @@ Right now the exporter will find any Vault PKI secrets engines and attempt to ge ## Contributing +Make sure run `pre-commit install` to install the various pre-commit linter and formatting hooks. + ### Testing Venom is used for tests, run `sudo venom run tests.yml` to perform integration tests. Make sure you have at least venom version 1.2.0. diff --git a/cmd/main.go b/cmd/main.go index 37b9a54..92c5270 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -86,7 +86,10 @@ func init() { } func main() { - cli.ParseFlags(os.Args[1:]) + err := cli.ParseFlags(os.Args[1:]) + if err != nil { + logger.SlogFatal("CLI parsing failed", "error", err) + } // preserve deprecated verbose flag if viper.GetBool("verbose") { @@ -98,7 +101,7 @@ func main() { // note mix of underscores and dashes slog.Info("CLI flag values", "fetch-interval", viper.GetDuration("fetch_interval"), "refresh-interval", viper.GetDuration("refresh_interval"), "batch-size-percent", viper.GetFloat64("batch_size_percent"), "request-limit", viper.GetFloat64("request_limit"), "request-limit-burst", viper.GetInt("request_limit_burst")) - err := cli.Execute() + err = cli.Execute() if err != nil { logger.SlogFatal("CLI execution failed", "error", err) } diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index d852c88..cc40bb1 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -5,7 +5,7 @@ import ( "os" ) -// Initialize default logger +// Init initializes default logger func Init(level string) { var slogLevel slog.Level switch level { diff --git a/pkg/vault-mon/influx.go b/pkg/vault-mon/influx.go index 29eac2f..5906b69 100644 --- a/pkg/vault-mon/influx.go +++ b/pkg/vault-mon/influx.go @@ -1,4 +1,4 @@ -package vault_mon +package vaultmon import ( "crypto/x509" @@ -13,6 +13,7 @@ import ( var hostname string +// InfluxWatchCerts looks for new certificate data func InfluxWatchCerts(pkimon *PKIMon, interval time.Duration, loop bool) { hostname = os.Getenv("HOSTNAME") if hostname == "" { diff --git a/pkg/vault-mon/pki.go b/pkg/vault-mon/pki.go index b66a540..c2f2d18 100644 --- a/pkg/vault-mon/pki.go +++ b/pkg/vault-mon/pki.go @@ -1,4 +1,4 @@ -package vault_mon +package vaultmon import ( "context" @@ -18,6 +18,7 @@ import ( "golang.org/x/time/rate" ) +// PKI has details about the Vault PKI Secrets Engine type PKI struct { path string certs map[string]map[string]*x509.Certificate @@ -29,6 +30,7 @@ type PKI struct { crlmux sync.Mutex } +// PKIMon helps watch all the possible PKI secrets engines type PKIMon struct { pkis map[string]*PKI vault *vaultapi.Client @@ -48,6 +50,7 @@ var loadCertsLimitDuration = promauto.NewHistogram(prometheus.HistogramOpts{ Buckets: prometheus.ExponentialBuckets(1, 3, 10), }) +// Init makes a new Vault client func (mon *PKIMon) Init(vault *vaultapi.Client) error { mon.vault = vault mon.pkis = make(map[string]*PKI) @@ -79,6 +82,7 @@ func (mon *PKIMon) loadPKI() error { return nil } +// Watch is a continuous loop to load cert and CRL data for later analysis func (mon *PKIMon) Watch(interval time.Duration) { slog.Info("Start watching PKI certs") @@ -111,6 +115,7 @@ func (mon *PKIMon) Watch(interval time.Duration) { }() } +// GetPKIs returns PKI engines func (mon *PKIMon) GetPKIs() map[string]*PKI { mon.mux.Lock() defer mon.mux.Unlock() @@ -371,12 +376,14 @@ func (pki *PKI) clearCerts() { pki.certsmux.Unlock() } +// GetCRLs is a thread safe way to return all CRLs used by various PKI engines func (pki *PKI) GetCRLs() map[string]*x509.RevocationList { pki.crlmux.Lock() defer pki.crlmux.Unlock() return pki.crls } +// GetCerts is a thread safe way to return all certs used by various PKI engines func (pki *PKI) GetCerts() map[string]map[string]*x509.Certificate { pki.certsmux.Lock() defer pki.certsmux.Unlock() diff --git a/pkg/vault-mon/prometheus.go b/pkg/vault-mon/prometheus.go index 72dbd5a..bb6583f 100644 --- a/pkg/vault-mon/prometheus.go +++ b/pkg/vault-mon/prometheus.go @@ -1,4 +1,4 @@ -package vault_mon +package vaultmon import ( "crypto/x509" @@ -25,6 +25,7 @@ var labelNames = []string{ "locality", } +// PromWatchCerts goes through all available certificates and updates metrics about them. Also deletes any certificate time series found in various CRLs func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { expiry := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_cert_expiry", @@ -42,21 +43,21 @@ func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { Name: "x509_cert_count", Help: "Total count of non-expired certificates including revoked certificates", }, []string{"source"}) - expired_cert_count := promauto.NewGaugeVec(prometheus.GaugeOpts{ + expiredCertCount := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_expired_cert_count", Help: "Total count of expired certificates including revoked certificates", }, []string{"source"}) - crl_expiry := promauto.NewGaugeVec(prometheus.GaugeOpts{ + crlExpiry := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_crl_expiry", }, []string{"source", "issuer"}) - crl_nextupdate := promauto.NewGaugeVec(prometheus.GaugeOpts{ + crlNextupdate := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_crl_nextupdate", }, []string{"source", "issuer"}) - crl_byte_size := promauto.NewGaugeVec(prometheus.GaugeOpts{ + crlByteSize := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_crl_byte_size", Help: "Size of raw certificate revocation list pem stored in vault", }, []string{"source", "issuer"}) - crl_length := promauto.NewGaugeVec(prometheus.GaugeOpts{ + crlLength := promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "x509_crl_length", Help: "Length of certificate revocation list", }, []string{"source", "issuer"}) @@ -81,10 +82,10 @@ func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { 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.RevokedCertificateEntries))) - crl_byte_size.WithLabelValues(pkiname, issuer).Set(float64(pki.crlRawSize)) + crlExpiry.WithLabelValues(pkiname, issuer).Set(float64(crl.NextUpdate.Sub(now).Seconds())) + crlNextupdate.WithLabelValues(pkiname, issuer).Set(float64(crl.NextUpdate.Unix())) + crlLength.WithLabelValues(pkiname, issuer).Set(float64(len(crl.RevokedCertificateEntries))) + crlByteSize.WithLabelValues(pkiname, issuer).Set(float64(pki.crlRawSize)) slog.Debug("Updated CRL metrics", "pki", pkiname, "issuer", issuer, "next_update", crl.NextUpdate) @@ -124,7 +125,7 @@ func PromWatchCerts(pkimon *PKIMon, interval time.Duration) { slog.Debug("Updated certificate metrics", "pki", pkiname, "serial", cert.SerialNumber, "common_name", cert.Subject.CommonName, "organizational_unit", cert.Subject.OrganizationalUnit) } certcount.WithLabelValues(pkiname).Set(float64(len(pki.certs))) - expired_cert_count.WithLabelValues(pkiname).Set(float64(pki.expiredCertsCounter)) + expiredCertCount.WithLabelValues(pkiname).Set(float64(pki.expiredCertsCounter)) } duration := time.Since(startTime).Seconds() @@ -149,6 +150,7 @@ func getLabelValues(pkiname string, cert *x509.Certificate) []string { } } +// PromStartExporter boots up the HTTP server to provide metrics func PromStartExporter(port int) { slog.Info("Starting Prometheus exporter", "port", port) http.Handle("/metrics", promhttp.Handler()) diff --git a/pkg/vault-mon/tools.go b/pkg/vault-mon/tools.go index 27b23c1..15efc5b 100644 --- a/pkg/vault-mon/tools.go +++ b/pkg/vault-mon/tools.go @@ -1,4 +1,4 @@ -package vault_mon +package vaultmon func getEmptyStringIfEmpty(data []string) string { if len(data) > 0 { diff --git a/pkg/vault/client.go b/pkg/vault/client.go index be9c2fc..f0320ff 100644 --- a/pkg/vault/client.go +++ b/pkg/vault/client.go @@ -15,25 +15,31 @@ import ( ) type secretCallback func(secret *vaultapi.Secret) -type secretKV2Callback func(secret *KV_version2) +type secretKV2Callback func(secret *KVVersion2) + +// ClientWrapper wraps around vaultapi client type ClientWrapper struct { Client *vaultapi.Client } -type KV_version2 struct { +// KVVersion2 is Vault's newer API version structure +type KVVersion2 struct { Data map[string]interface{} `json:"data"` Metadata map[string]interface{} `json:"metadata"` } +// SecretList is a list of keys type SecretList struct { Keys []string } +// SecretCertificate is an object of the actual certificate we obtain from Vault type SecretCertificate struct { - Certificate string - Revocation_time int64 + Certificate string + RevocationTime int64 } +// Init makes a new Vault Client Wrapper func (vault *ClientWrapper) Init() { var err error @@ -57,15 +63,15 @@ func (vault *ClientWrapper) Init() { } } - token_secret, err := vault.Client.Auth().Token().LookupSelf() + tokenSecret, err := vault.Client.Auth().Token().LookupSelf() if err != nil { logger.SlogFatal("[vault] Error getting a new token", err) } - ttl, _ := token_secret.TokenTTL() + ttl, _ := tokenSecret.TokenTTL() - slog.Info("Token TTL and LeaseDuration", "ttl", int32(ttl/time.Second), "lease_duration", token_secret.LeaseDuration) + slog.Info("Token TTL and LeaseDuration", "ttl", int32(ttl/time.Second), "lease_duration", tokenSecret.LeaseDuration) - isRenewable, _ := token_secret.TokenIsRenewable() + isRenewable, _ := tokenSecret.TokenIsRenewable() if isRenewable { // Get a renewed token secret, err := vault.Client.Auth().Token().RenewTokenAsSelf(vault.Client.Token(), 0) @@ -73,20 +79,21 @@ func (vault *ClientWrapper) Init() { logger.SlogFatal("[vault] Error renewing token", err) } - token_renewer, err := vault.Client.NewRenewer(&vaultapi.RenewerInput{ + tokenRenewer, err := vault.Client.NewLifetimeWatcher(&vaultapi.RenewerInput{ Secret: secret, }) if err != nil { logger.SlogFatal("[vault] Error renewing token", err) } - watch_renewer_vault(token_renewer) + watchRenewerVault(tokenRenewer) } else { - ttl, _ := token_secret.TokenTTL() + ttl, _ := tokenSecret.TokenTTL() slog.Info("[vault] token is not renewable", "ttl", int32(ttl/time.Second)) } } +// GetSecret finds the secrets to authenticate with Vault func (vault *ClientWrapper) GetSecret(path string, fn secretCallback) error { var secret *vaultapi.Secret var err error @@ -101,14 +108,14 @@ func (vault *ClientWrapper) GetSecret(path string, fn secretCallback) error { fn(secret) if secret.Renewable { - renewer, err := vault.Client.NewRenewer(&vaultapi.RenewerInput{ + renewer, err := vault.Client.NewLifetimeWatcher(&vaultapi.RenewerInput{ Secret: secret, }) if err != nil { logger.SlogFatal("[vault] Error renewing token", err) } - watch_renewer_vault(renewer) + watchRenewerVault(renewer) } else { slog.Info("[vault] secret is not renewable, use TTL to refresh secret", "path", path) // Refresh secret at the end of Lease @@ -135,18 +142,19 @@ func (vault *ClientWrapper) GetSecret(path string, fn secretCallback) error { return nil } +// GetSecretKV2 gets the Vault auth secret for the KV2 API func (vault *ClientWrapper) GetSecretKV2(path string, fn secretKV2Callback) error { - var secret_kv2 = &KV_version2{} + var secretKv2 = &KVVersion2{} return vault.GetSecret(path, func(secret *vaultapi.Secret) { - err := mapstructure.WeakDecode(secret.Data, secret_kv2) + err := mapstructure.WeakDecode(secret.Data, secretKv2) if err != nil { - slog.Error("Can decode secret as KV version 2", "error", err) + slog.Error("Cannot decode secret for KV version 2", "error", err) } - fn(secret_kv2) + fn(secretKv2) }) } -func watch_renewer_vault(renewer *vaultapi.Renewer) { +func watchRenewerVault(renewer *vaultapi.Renewer) { go func() { for { select {