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

Allow reading certificates from ConfigMap resources #324

Merged
merged 4 commits into from
Dec 10, 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
4 changes: 4 additions & 0 deletions cmd/x509-certificate-exporter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ func main() {
kubeSecretTypes := stringArrayFlag{}
getopt.FlagLong(&kubeSecretTypes, "secret-type", 's', "one or more kubernetes secret type & key to watch (e.g. \"kubernetes.io/tls:tls.crt\"")

kubeConfigMapKeys := stringArrayFlag{}
getopt.FlagLong(&kubeConfigMapKeys, "configmap-keys", 'c', "keys in configmaps to watch")

kubeIncludeNamespaces := stringArrayFlag{}
getopt.FlagLong(&kubeIncludeNamespaces, "include-namespace", 0, "add the given kube namespace to the watch list (when used, all namespaces are excluded by default)")

Expand Down Expand Up @@ -112,6 +115,7 @@ func main() {
ExposeRelativeMetrics: *exposeRelativeMetrics,
ExposeErrorMetrics: *exposeErrorMetrics,
KubeSecretTypes: kubeSecretTypes,
ConfigMapKeys: kubeConfigMapKeys,
KubeIncludeNamespaces: kubeIncludeNamespaces,
KubeExcludeNamespaces: kubeExcludeNamespaces,
KubeIncludeLabels: kubeIncludeLabels,
Expand Down
3 changes: 2 additions & 1 deletion deploy/charts/x509-certificate-exporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ hostPathsExporter:
| secretsExporter.extraVolumes | list | `[]` | Additionnal volumes added to Pods of the TLS Secrets exporter (combined with global `extraVolumes`) |
| secretsExporter.extraVolumeMounts | list | `[]` | Additionnal volume mounts added to Pod containers of the TLS Secrets exporter (combined with global `extraVolumeMounts`) |
| secretsExporter.secretTypes | list | check `values.yaml` | Which type of Secrets should be watched ; "key" is the map key in the secret data |
| secretsExporter.configMapKeys | list | check `values.yaml` | If the exporter should for certificates in configmaps, just specify the keys it needs to watch. E.g.: `configMapKeys: ["tls.crt"]` |
| secretsExporter.includeNamespaces | list | `[]` | Restrict the list of namespaces the TLS Secrets exporter should scan for certificates to watch (all namespaces if empty) |
| secretsExporter.excludeNamespaces | list | `[]` | Exclude namespaces from being scanned by the TLS Secrets exporter (evaluated after `includeNamespaces`) |
| secretsExporter.includeLabels | list | `[]` | Only watch TLS Secrets having these labels (all secrets if empty). Items can be keys such as `my-label` or also require a value with syntax `my-label=my-value`. |
Expand Down Expand Up @@ -427,7 +428,7 @@ hostPathsExporter:
| prometheusServiceMonitor.scrapeInterval | string | `"60s"` | Target scrape interval set in the ServiceMonitor |
| prometheusServiceMonitor.scrapeTimeout | string | `"30s"` | Target scrape timeout set in the ServiceMonitor |
| prometheusServiceMonitor.extraLabels | object | `{}` | Additional labels to add to ServiceMonitor objects |
| prometheusServiceMonitor.metricRelabelings | list | `[]` | Metrics relabel config for the ServiceMonitor, see: https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#monitoring.coreos.com/v1.Endpoint |
| prometheusServiceMonitor.metricRelabelings | list | `[]` | Metric relabel config for the ServiceMonitor, see: https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#monitoring.coreos.com/v1.Endpoint |
| prometheusServiceMonitor.relabelings | list | `[]` | Relabel config for the ServiceMonitor, see: https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#monitoring.coreos.com/v1.Endpoint |
| prometheusPodMonitor.create | bool | `false` | Should a PodMonitor object be installed to scrape this exporter. For prometheus-operator (kube-prometheus) users. |
| prometheusPodMonitor.scrapeInterval | string | `"60s"` | Target scrape interval set in the PodMonitor |
Expand Down
10 changes: 10 additions & 0 deletions deploy/charts/x509-certificate-exporter/templates/clusterrole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ rules:
- get
- watch
- list
{{- if .Values.secretsExporter.configMapKeys }}
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- watch
- list
{{- end }}
{{- if .Values.rbacProxy.enable }}
- apiGroups:
- authentication.k8s.io
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ spec:
{{- range .Values.secretsExporter.secretTypes }}
- --secret-type={{ .type | trim }}:{{ .key | trim }}
{{- end }}
{{- range .Values.secretsExporter.configMapKeys }}
- --configmap-keys={{ . | trim }}
{{- end }}
{{- range .Values.secretsExporter.includeNamespaces }}
- --include-namespace={{ . | trim }}
{{- end }}
Expand Down
4 changes: 4 additions & 0 deletions deploy/charts/x509-certificate-exporter/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ secretsExporter:
- type: kubernetes.io/tls
key: tls.crt

# -- If the exporter should for certificates in configmaps, just specify the keys it needs to watch. E.g.: `configMapKeys: ["tls.crt"]`
# @default -- check `values.yaml`
configMapKeys: []

# -- Restrict the list of namespaces the TLS Secrets exporter should scan for certificates to watch (all namespaces if empty)
includeNamespaces: []
# -- Exclude namespaces from being scanned by the TLS Secrets exporter (evaluated after `includeNamespaces`)
Expand Down
37 changes: 27 additions & 10 deletions internal/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type certificateRef struct {
certificates []*parsedCertificate
yamlPaths []YAMLCertRef
kubeSecret v1.Secret
kubeConfigMap v1.ConfigMap
kubeSecretKey string
}

Expand All @@ -84,9 +85,10 @@ type certificateError struct {
type certificateFormat int

const (
certificateFormatPEM certificateFormat = iota
certificateFormatYAML = iota
certificateFormatKubeSecret = iota
certificateFormatPEM certificateFormat = iota
certificateFormatYAML = iota
certificateFormatKubeSecret = iota
certificateFormatKubeConfigMap = iota
)

func (cert *certificateRef) parse() error {
Expand All @@ -99,8 +101,9 @@ func (cert *certificateRef) parse() error {
cert.certificates, err = readAndParseYAMLFile(cert.path, cert.yamlPaths)
case certificateFormatKubeSecret:
cert.certificates, err = readAndParseKubeSecret(&cert.kubeSecret, cert.kubeSecretKey)
case certificateFormatKubeConfigMap:
cert.certificates, err = readAndParseKubeConfigMap(&cert.kubeConfigMap, cert.kubeSecretKey)
}

return err
}

Expand Down Expand Up @@ -210,22 +213,36 @@ func readAndParseYAMLFile(filePath string, yamlPaths []YAMLCertRef) ([]*parsedCe
return output, nil
}

func readAndParseKubeSecret(secret *v1.Secret, key string) ([]*parsedCertificate, error) {
certs, err := parsePEM(secret.Data[key])
func parseKubeCerts(certs []byte) ([]*parsedCertificate, error) {
output := []*parsedCertificate{}
parsedCerts, err := parsePEM(certs)
if err != nil {
return nil, err
}

output := []*parsedCertificate{}
for _, cert := range certs {
for _, cert := range parsedCerts {
output = append(output, &parsedCertificate{
cert: cert,
})
}

return output, nil
}

func readAndParseKubeConfigMap(cm *v1.ConfigMap, key string) ([]*parsedCertificate, error) {
certs_data, ok := cm.Data[key]
if !ok {
return nil, fmt.Errorf("key %s not found in ConfigMap %s/%s", key, cm.Namespace, cm.Name)
}
return parseKubeCerts([]byte(certs_data))
}

func readAndParseKubeSecret(secret *v1.Secret, key string) ([]*parsedCertificate, error) {
certs, ok := secret.Data[key]
if !ok {
return nil, fmt.Errorf("key %s not found in Secret %s/%s", key, secret.Namespace, secret.Name)
}
return parseKubeCerts(certs)
}

func readFile(file string) ([]byte, error) {
contents, err := os.ReadFile(file)
if err == nil || !os.IsNotExist(err) {
Expand Down
15 changes: 9 additions & 6 deletions internal/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,18 @@ type Exporter struct {
ExposeErrorMetrics bool
ExposeLabels []string
KubeSecretTypes []string
ConfigMapKeys []string
KubeIncludeNamespaces []string
KubeExcludeNamespaces []string
KubeIncludeLabels []string
KubeExcludeLabels []string

kubeClient *kubernetes.Clientset
listener net.Listener
server *http.Server
isDiscovery bool
secretsCache *cache.Cache
kubeClient *kubernetes.Clientset
listener net.Listener
server *http.Server
isDiscovery bool
secretsCache *cache.Cache
configMapsCache *cache.Cache
}

// ListenAndServe : Convenience function to start exporter
Expand Down Expand Up @@ -117,6 +119,7 @@ func (exporter *Exporter) Shutdown() error {
// DiscoverCertificates : Parse all certs in a dry run with verbose logging
func (exporter *Exporter) DiscoverCertificates() {
exporter.secretsCache = cache.New(exporter.MaxCacheDuration, 5*time.Minute)
exporter.configMapsCache = cache.New(exporter.MaxCacheDuration, 5*time.Minute)
exporter.isDiscovery = true
certs, errs := exporter.parseAllCertificates()

Expand Down Expand Up @@ -176,7 +179,7 @@ func (exporter *Exporter) parseAllCertificates() ([]*certificateRef, []*certific
}

if exporter.kubeClient != nil {
certs, errs := exporter.parseAllKubeSecrets()
certs, errs := exporter.parseAllKubeObjects()
output = append(output, certs...)
for _, err := range errs {
raiseError(&certificateError{
Expand Down
78 changes: 63 additions & 15 deletions internal/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,49 @@ func (exporter *Exporter) ConnectToKubernetesCluster(path string, rateLimiter fl
return err
}

func (exporter *Exporter) parseAllKubeSecrets() ([]*certificateRef, []error) {
func (exporter *Exporter) parseAllKubeObjects() ([]*certificateRef, []error) {
output := []*certificateRef{}
outputErrors := []error{}
readCertificatesFromSecrets := func(secrets []v1.Secret) (outputs []*certificateRef) {
for _, secret := range secrets {
for _, secretType := range exporter.KubeSecretTypes {
typeAndKey := strings.Split(secretType, ":")

if secret.Type == v1.SecretType(typeAndKey[0]) && len(secret.Data[typeAndKey[1]]) > 0 {
outputs = append(outputs, &certificateRef{
path: fmt.Sprintf("k8s/%s/%s", secret.GetNamespace(), secret.GetName()),
format: certificateFormatKubeSecret,
kubeSecret: secret,
kubeSecretKey: typeAndKey[1],
})
}
}
}
return outputs
}
contains := func(needle string, haystack []string) bool {
for _, item := range haystack {
if needle == item {
return true
}
}
return false
}
readCertificatesFromConfigMaps := func(configMaps []v1.ConfigMap) (outputs []*certificateRef) {
for _, configMap := range configMaps {
for key, _ := range configMap.Data {
if contains(key, exporter.ConfigMapKeys) {
outputs = append(outputs, &certificateRef{
path: fmt.Sprintf("k8s/%s/%s", configMap.GetNamespace(), configMap.GetName()),
format: certificateFormatKubeConfigMap,
kubeConfigMap: configMap,
kubeSecretKey: key,
})
}
}
}
return outputs
}

namespaces, err := exporter.listNamespacesToWatch()
if err != nil {
Expand All @@ -42,21 +82,13 @@ func (exporter *Exporter) parseAllKubeSecrets() ([]*certificateRef, []error) {
outputErrors = append(outputErrors, fmt.Errorf("failed to fetch secrets from namespace \"%s\": %s", namespace, err.Error()))
continue
}

for _, secret := range secrets {
for _, secretType := range exporter.KubeSecretTypes {
typeAndKey := strings.Split(secretType, ":")

if secret.Type == v1.SecretType(typeAndKey[0]) && len(secret.Data[typeAndKey[1]]) > 0 {
output = append(output, &certificateRef{
path: fmt.Sprintf("k8s/%s/%s", namespace, secret.GetName()),
format: certificateFormatKubeSecret,
kubeSecret: secret,
kubeSecretKey: typeAndKey[1],
})
}
}
configMaps, err := exporter.getWatchedConfigMaps(namespace)
if err != nil {
outputErrors = append(outputErrors, fmt.Errorf("failed to fetch configmaps from namespace \"%s\": %s", namespace, err.Error()))
continue
}
output = append(output, readCertificatesFromSecrets(secrets)...)
output = append(output, readCertificatesFromConfigMaps(configMaps)...)
}

return output, outputErrors
Expand Down Expand Up @@ -95,6 +127,22 @@ func (exporter *Exporter) listNamespacesToWatch() ([]string, error) {
return namespaces, nil
}

func (exporter *Exporter) getWatchedConfigMaps(namespace string) ([]v1.ConfigMap, error) {
cachedConfigMaps, cached := exporter.configMapsCache.Get(namespace)
if cached {
return cachedConfigMaps.([]v1.ConfigMap), nil
}
configMapsList, err := exporter.kubeClient.CoreV1().ConfigMaps(namespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, err
}
configMaps := configMapsList.Items
halfDuration := float64(exporter.MaxCacheDuration.Nanoseconds()) / 2
cacheDuration := halfDuration*float64(rand.Float64()) + halfDuration
exporter.configMapsCache.Set(namespace, configMaps, time.Duration(cacheDuration))
return configMaps, nil
}

func (exporter *Exporter) getWatchedSecrets(namespace string) ([]v1.Secret, error) {
cachedSecrets, cached := exporter.secretsCache.Get(namespace)
if cached {
Expand Down