Skip to content

Commit d674c3b

Browse files
feat(client): minimize snapshot by filtering non-clientauth TLS secrets
- Add minimizeSnapshot to remove TLS secrets without client certificates - Improve privacy and reduce bandwidth/storage for CyberArk uploads - Implement isTLSSecretWithoutClientCert to detect client certificates - Add PEM parsing and client certificate detection helpers Signed-off-by: Richard Wall <richard.wall@cyberark.com>
1 parent 7b58625 commit d674c3b

File tree

2 files changed

+426
-0
lines changed

2 files changed

+426
-0
lines changed

pkg/client/client_cyberark.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@ package client
22

33
import (
44
"context"
5+
"crypto/x509"
6+
"encoding/base64"
7+
"encoding/pem"
58
"fmt"
69
"net/http"
710

11+
"github.com/go-logr/logr"
12+
corev1 "k8s.io/api/core/v1"
13+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
814
"k8s.io/apimachinery/pkg/runtime"
915
"k8s.io/apimachinery/pkg/util/sets"
16+
"k8s.io/klog/v2"
1017

1118
"github.com/jetstack/preflight/api"
1219
"github.com/jetstack/preflight/internal/cyberark"
1320
"github.com/jetstack/preflight/internal/cyberark/dataupload"
21+
"github.com/jetstack/preflight/pkg/logs"
1422
"github.com/jetstack/preflight/pkg/version"
1523
)
1624

@@ -44,10 +52,13 @@ func NewCyberArk(httpClient *http.Client) (*CyberArkClient, error) {
4452
// then uploads a snapshot.
4553
// The supplied Options are not used by this publisher.
4654
func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readings []*api.DataReading, _ Options) error {
55+
log := klog.FromContext(ctx)
4756
var snapshot dataupload.Snapshot
4857
if err := convertDataReadings(defaultExtractorFunctions, readings, &snapshot); err != nil {
4958
return fmt.Errorf("while converting data readings: %s", err)
5059
}
60+
minimizeSnapshot(log, &snapshot)
61+
5162
snapshot.AgentVersion = version.PreflightVersion
5263

5364
cfg, err := o.configLoader()
@@ -66,6 +77,35 @@ func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readin
6677
return nil
6778
}
6879

80+
// minimizeSnapshot reduces the size of the snapshot by removing unnecessary data.
81+
//
82+
// This reduces the bandwidth used when uploading the snapshot to CyberArk,
83+
// it reduces the storage used by CyberArk to store the snapshot, and
84+
// it provides better privacy for the cluster being scanned; only the necessary
85+
// data is included in the snapshot.
86+
//
87+
// This is a best-effort attempt to minimize the snapshot size. Errors during
88+
// minimization are logged but do not prevent the snapshot from being uploaded.
89+
//
90+
// It performs the following minimization steps:
91+
//
92+
// 1. Removal of non-clientauth TLS secrets: It filters out TLS secrets that do
93+
// not contain a client certificate. This is done to avoid uploading large
94+
// TLS secrets that are not relevant for the CyberArk Discovery and Context
95+
// service.
96+
func minimizeSnapshot(log logr.Logger, snapshot *dataupload.Snapshot) {
97+
originalSecretCount := len(snapshot.Secrets)
98+
filteredSecrets := make([]runtime.Object, 0, originalSecretCount)
99+
for _, secret := range snapshot.Secrets {
100+
if isTLSSecretWithoutClientCert(log, secret) {
101+
continue
102+
}
103+
filteredSecrets = append(filteredSecrets, secret)
104+
}
105+
snapshot.Secrets = filteredSecrets
106+
log.V(logs.Debug).Info("Minimized snapshot", "originalSecretCount", originalSecretCount, "filteredSecretCount", len(snapshot.Secrets))
107+
}
108+
69109
// extractClusterIDAndServerVersionFromReading converts the opaque data from a DiscoveryData
70110
// data reading to allow access to the Kubernetes version fields within.
71111
func extractClusterIDAndServerVersionFromReading(reading *api.DataReading, target *dataupload.Snapshot) error {
@@ -190,3 +230,118 @@ func convertDataReadings(
190230
}
191231
return nil
192232
}
233+
234+
// isTLSSecretWithoutClientCert filters out all TLS secrets that do not
235+
// contain a client certificate in the `tls.crt` key.
236+
// Secrets are obtained by a DynamicClient, so they have type
237+
// *unstructured.Unstructured.
238+
func isTLSSecretWithoutClientCert(log logr.Logger, obj runtime.Object) bool {
239+
logTrace := log.V(logs.Trace)
240+
// Fast path: type assertion and kind/type checks
241+
unstructuredObj, ok := obj.(*unstructured.Unstructured)
242+
if !ok {
243+
logTrace.Info("Object is not a Unstructured", "type", fmt.Sprintf("%T", obj))
244+
return false
245+
}
246+
if unstructuredObj.GetKind() != "Secret" || unstructuredObj.GetAPIVersion() != "v1" {
247+
return false
248+
}
249+
250+
logTrace = logTrace.WithValues("namespace", unstructuredObj.GetNamespace(), "name", unstructuredObj.GetName())
251+
252+
secretType, found, err := unstructured.NestedString(unstructuredObj.Object, "type")
253+
if err != nil || !found || secretType != string(corev1.SecretTypeTLS) {
254+
logTrace.Info("Object is not a TLS Secret", "type", secretType)
255+
return false
256+
}
257+
258+
// Directly extract tls.crt from unstructured data (avoid conversion if possible)
259+
dataMap, found, err := unstructured.NestedMap(unstructuredObj.Object, "data")
260+
if err != nil || !found {
261+
logTrace.Info("Secret data missing or not a map")
262+
return true
263+
}
264+
tlsCrtRaw, found := dataMap[corev1.TLSCertKey]
265+
if !found {
266+
logTrace.Info("TLS Secret does not contain tls.crt key")
267+
return true
268+
}
269+
270+
// Decode base64 if necessary (K8s secrets store data as base64-encoded strings)
271+
var tlsCrtBytes []byte
272+
switch v := tlsCrtRaw.(type) {
273+
case string:
274+
decoded, err := base64.StdEncoding.DecodeString(v)
275+
if err != nil {
276+
logTrace.Info("Failed to decode tls.crt base64", "error", err.Error())
277+
return true
278+
}
279+
tlsCrtBytes = decoded
280+
case []byte:
281+
tlsCrtBytes = v
282+
default:
283+
logTrace.Info("tls.crt is not a string or byte slice", "type", fmt.Sprintf("%T", v))
284+
return true
285+
}
286+
287+
// Parse PEM certificate chain
288+
certs, err := parsePEMCertificateChain(tlsCrtBytes)
289+
if err != nil || len(certs) == 0 {
290+
logTrace.Info("Failed to parse tls.crt as PEM encoded X.509 certificate chain", "error", err.Error())
291+
return true
292+
}
293+
294+
// Check if the leaf certificate is a client certificate
295+
if isClientCertificate(certs[0]) {
296+
logTrace.Info("TLS Secret contains a client certificate")
297+
return false
298+
}
299+
300+
logTrace.Info("TLS Secret does not contain a client certificate")
301+
return true
302+
}
303+
304+
// isClientCertificate checks if the given certificate is a client certificate
305+
// by checking if it has the ClientAuth EKU.
306+
func isClientCertificate(cert *x509.Certificate) bool {
307+
if cert == nil {
308+
return false
309+
}
310+
// Check if the certificate has the ClientAuth EKU
311+
for _, eku := range cert.ExtKeyUsage {
312+
if eku == x509.ExtKeyUsageClientAuth {
313+
return true
314+
}
315+
}
316+
return false
317+
}
318+
319+
// parsePEMCertificateChain parses a PEM encoded certificate chain and returns
320+
// a slice of x509.Certificate pointers. It returns an error if the data cannot
321+
// be parsed as a certificate chain.
322+
// The supplied data can contain multiple PEM blocks, the function will parse
323+
// all of them and return a slice of certificates.
324+
func parsePEMCertificateChain(data []byte) ([]*x509.Certificate, error) {
325+
// Parse the PEM encoded certificate chain
326+
var certs []*x509.Certificate
327+
var block *pem.Block
328+
rest := data
329+
for {
330+
block, rest = pem.Decode(rest)
331+
if block == nil {
332+
break
333+
}
334+
if block.Type != "CERTIFICATE" || len(block.Bytes) == 0 {
335+
continue
336+
}
337+
cert, err := x509.ParseCertificate(block.Bytes)
338+
if err != nil {
339+
return nil, fmt.Errorf("failed to parse certificate: %w", err)
340+
}
341+
certs = append(certs, cert)
342+
}
343+
if len(certs) == 0 {
344+
return nil, fmt.Errorf("no certificates found")
345+
}
346+
return certs, nil
347+
}

0 commit comments

Comments
 (0)