Skip to content

Commit 5736d06

Browse files
feat(k8s): add filter for ignoring TLS secrets without client certificates
- Introduce IgnoreTLSSecretsContainingNonClientCertificates filter - Update dynamic gatherer config to support resource filters via YAML - Apply filters when adding resources to cache to exclude non-client TLS secrets Signed-off-by: Richard Wall <[email protected]>
1 parent 7b58625 commit 5736d06

File tree

5 files changed

+132
-2
lines changed

5 files changed

+132
-2
lines changed

deploy/charts/cyberark-disco-agent/templates/configmap.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ data:
3030
- type!=kubernetes.io/dockerconfigjson
3131
- type!=bootstrap.kubernetes.io/token
3232
- type!=helm.sh/release.v1
33+
filters:
34+
- IgnoreTLSSecretsContainingNonClientCertificates
3335
- kind: k8s-dynamic
3436
name: ark/serviceaccounts
3537
config:

examples/machinehub.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ data-gatherers:
2828
- type!=kubernetes.io/dockerconfigjson
2929
- type!=bootstrap.kubernetes.io/token
3030
- type!=helm.sh/release.v1
31+
filters:
32+
- IgnoreTLSSecretsContainingNonClientCertificates
3133

3234
# Gather Kubernetes service accounts
3335
- name: ark/serviceaccounts

pkg/datagatherer/k8s/cache.go

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package k8s
22

33
import (
4+
"crypto/x509"
5+
"encoding/pem"
46
"fmt"
57
"time"
68

79
"github.com/go-logr/logr"
810
"github.com/pmylund/go-cache"
11+
corev1 "k8s.io/api/core/v1"
912
"k8s.io/apimachinery/pkg/types"
1013

1114
"github.com/jetstack/preflight/api"
@@ -39,9 +42,113 @@ func logCacheUpdateFailure(log logr.Logger, obj interface{}, operation string) {
3942
log.Error(err, "Cache update failure", "operation", operation)
4043
}
4144

45+
// cacheFilterFunction is a function that takes an object and returns true if the
46+
// object should not be added to the cache, false otherwise.
47+
// This can be used to filter out objects that are not relevant for the data gatherer.
48+
type cacheFilterFunction func(interface{}) bool
49+
50+
// IgnoreTLSSecretsContainingNonClientCertificates filters out all TLS secrets that do not
51+
// contain a client certificate in the `tls.crt` key.
52+
func IgnoreTLSSecretsContainingNonClientCertificates(obj interface{}) bool {
53+
_, ok := obj.(cacheResource)
54+
if !ok {
55+
// Not a cacheResource, skip it
56+
// Programming error if this happens
57+
return false
58+
}
59+
secret, ok := obj.(*corev1.Secret)
60+
if !ok {
61+
// Not a Secret, skip it
62+
return false
63+
}
64+
// Check if the secret contains the `tls.crt` key
65+
data, found := secret.Data[corev1.TLSCertKey]
66+
// If the key is not found or the data is empty, skip it
67+
68+
if !found || len(data) == 0 {
69+
return false
70+
}
71+
72+
// Try to parse the data as a PEM encoded X.509 certificate chain
73+
certs, err := parsePEMCertificateChain(data)
74+
if err != nil {
75+
return false
76+
}
77+
78+
if len(certs) == 0 {
79+
return false
80+
}
81+
82+
// Check if the leaf certificate is a client certificate
83+
if isClientCertificate(certs[0]) {
84+
return false
85+
}
86+
87+
// Not a client certificate, drop it
88+
return true
89+
}
90+
91+
// isClientCertificate checks if the given certificate is a client certificate
92+
// by checking if it has the ClientAuth EKU.
93+
func isClientCertificate(cert *x509.Certificate) bool {
94+
if cert == nil {
95+
return false
96+
}
97+
// Check if the certificate has the ClientAuth EKU
98+
for _, eku := range cert.ExtKeyUsage {
99+
if eku == x509.ExtKeyUsageClientAuth {
100+
return true
101+
}
102+
}
103+
return false
104+
}
105+
106+
// parsePEMCertificateChain parses a PEM encoded certificate chain and returns
107+
// a slice of x509.Certificate pointers. It returns an error if the data cannot
108+
// be parsed as a certificate chain.
109+
// The supplied data can contain multiple PEM blocks, the function will parse
110+
// all of them and return a slice of certificates.
111+
func parsePEMCertificateChain(data []byte) ([]*x509.Certificate, error) {
112+
// Parse the PEM encoded certificate chain
113+
var certs []*x509.Certificate
114+
var block *pem.Block
115+
rest := data
116+
for {
117+
block, rest = pem.Decode(rest)
118+
if block == nil {
119+
break
120+
}
121+
if block.Type != "CERTIFICATE" || len(block.Bytes) == 0 {
122+
continue
123+
}
124+
cert, err := x509.ParseCertificate(block.Bytes)
125+
if err != nil {
126+
return nil, fmt.Errorf("failed to parse certificate: %w", err)
127+
}
128+
certs = append(certs, cert)
129+
}
130+
if len(certs) == 0 {
131+
return nil, fmt.Errorf("no certificates found")
132+
}
133+
return certs, nil
134+
}
135+
42136
// onAdd handles the informer creation events, adding the created runtime.Object
43137
// to the data gatherer's cache. The cache key is the uid of the object
44-
func onAdd(log logr.Logger, obj interface{}, dgCache *cache.Cache) {
138+
// The object is wrapped in a GatheredResource struct.
139+
// If the object is already present in the cache, it gets replaced.
140+
// The cache key is the uid of the object
141+
// The supplied filter functions can be used to filter out objects that
142+
// should not be added to the cache.
143+
// If multiple filter functions are supplied, the object is filtered out
144+
// if any of the filter functions returns true.
145+
func onAdd(log logr.Logger, obj interface{}, dgCache *cache.Cache, filters ...cacheFilterFunction) {
146+
for _, filter := range filters {
147+
if filter != nil && filter(obj) {
148+
return
149+
}
150+
}
151+
45152
item, ok := obj.(cacheResource)
46153
if ok {
47154
cacheObject := &api.GatheredResource{

pkg/datagatherer/k8s/dynamic.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ type ConfigDynamic struct {
4343
IncludeNamespaces []string `yaml:"include-namespaces"`
4444
// FieldSelectors is a list of field selectors to use when listing this resource
4545
FieldSelectors []string `yaml:"field-selectors"`
46+
// Filters is a list of filter functions to apply to the resources before adding them to the cache.
47+
// Each filter function should return true if the resource should be excluded, false otherwise.
48+
// Available filter functions:
49+
// - IgnoreTLSSecretsContainingNonClientCertificates: ignores all TLS
50+
// secrets that do not contain client certificates
51+
Filters []cacheFilterFunction `yaml:"filters"`
4652
}
4753

4854
// UnmarshalYAML unmarshals the ConfigDynamic resolving GroupVersionResource.
@@ -57,6 +63,7 @@ func (c *ConfigDynamic) UnmarshalYAML(unmarshal func(interface{}) error) error {
5763
ExcludeNamespaces []string `yaml:"exclude-namespaces"`
5864
IncludeNamespaces []string `yaml:"include-namespaces"`
5965
FieldSelectors []string `yaml:"field-selectors"`
66+
Filters []string `yaml:"filters"`
6067
}{}
6168
err := unmarshal(&aux)
6269
if err != nil {
@@ -71,6 +78,15 @@ func (c *ConfigDynamic) UnmarshalYAML(unmarshal func(interface{}) error) error {
7178
c.IncludeNamespaces = aux.IncludeNamespaces
7279
c.FieldSelectors = aux.FieldSelectors
7380

81+
for _, filterName := range aux.Filters {
82+
switch filterName {
83+
case "IgnoreTLSSecretsContainingNonClientCertificates":
84+
c.Filters = append(c.Filters, IgnoreTLSSecretsContainingNonClientCertificates)
85+
default:
86+
return fmt.Errorf("filters contains an unknown filter function: %s. Must be one of: IgnoreTLSSecretsContainingNonClientCertificates", filterName)
87+
}
88+
}
89+
7490
return nil
7591
}
7692

@@ -218,7 +234,7 @@ func (c *ConfigDynamic) newDataGathererWithClient(ctx context.Context, cl dynami
218234

219235
registration, err := newDataGatherer.informer.AddEventHandlerWithOptions(k8scache.ResourceEventHandlerFuncs{
220236
AddFunc: func(obj interface{}) {
221-
onAdd(log, obj, dgCache)
237+
onAdd(log, obj, dgCache, c.Filters...)
222238
},
223239
UpdateFunc: func(oldObj, newObj interface{}) {
224240
onUpdate(log, oldObj, newObj, dgCache)

pkg/datagatherer/k8s/dynamic_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ include-namespaces:
217217
- default
218218
field-selectors:
219219
- type!=kubernetes.io/service-account-token
220+
filters:
221+
- IgnoreTLSSecretsContainingNonClientCertificates
220222
`
221223

222224
expectedGVR := schema.GroupVersionResource{
@@ -259,6 +261,7 @@ field-selectors:
259261
if got, want := cfg.FieldSelectors, expectedFieldSelectors; !reflect.DeepEqual(got, want) {
260262
t.Errorf("FieldSelectors does not match: got=%+v want=%+v", got, want)
261263
}
264+
assert.Equal(t, []cacheFilterFunction{IgnoreTLSSecretsContainingNonClientCertificates}, cfg.Filters)
262265
}
263266

264267
func TestConfigDynamicValidate(t *testing.T) {

0 commit comments

Comments
 (0)