diff --git a/pkg/datagatherer/k8s/dynamic.go b/pkg/datagatherer/k8s/dynamic.go index b0e1dedf..50b6c0a4 100644 --- a/pkg/datagatherer/k8s/dynamic.go +++ b/pkg/datagatherer/k8s/dynamic.go @@ -350,6 +350,18 @@ func (g *DataGathererDynamic) Fetch() (interface{}, int, error) { return list, len(items), nil } +// redactList removes sensitive and superfluous data from the supplied resource list. +// All resources have superfluous managed-data fields removed. +// All resources have sensitive labels and annotations removed. +// Secret and Route are processed as special cases. For these +// resources there is an allow-list of fields that should be retained. +// For Secret resources, the `data` is redacted, to prevent private keys or sensitive +// data being collected; only the tls.crt and ca.crt data keys are retained. +// For Route resources, only the fields related to CA certificate and policy are retained. +// TODO(wallrj): A short coming of the current allow-list implementation is that +// you have to specify absolute fields paths. It is not currently possible to +// select all metadata with: `{metadata}`. This means that the metadata for +// Secret and Route has fewer fields than the metadata for all other resources. func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys []*regexp.Regexp) error { for i := range list { if item, ok := list[i].Resource.(*unstructured.Unstructured); ok { @@ -361,7 +373,7 @@ func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys resource := item - // Redact item if it is a: + // Redact item if it is a Secret or a Route. for _, gvk := range gvks { // secret object if gvk.Kind == "Secret" && (gvk.Group == "core" || gvk.Group == "") { diff --git a/pkg/datagatherer/k8s/fieldfilter.go b/pkg/datagatherer/k8s/fieldfilter.go index ed39acb3..566ab0a2 100644 --- a/pkg/datagatherer/k8s/fieldfilter.go +++ b/pkg/datagatherer/k8s/fieldfilter.go @@ -5,7 +5,10 @@ import ( ) // SecretSelectedFields is the list of fields sent from Secret objects to the -// backend +// backend. +// The `data` is redacted, to prevent private keys or sensitive data being +// collected. Only the following none-sensitive keys are retained: tls.crt, +// ca.crt. These keys are assumed to always contain public TLS certificates. var SecretSelectedFields = []FieldPath{ {"kind"}, {"apiVersion"}, @@ -16,6 +19,9 @@ var SecretSelectedFields = []FieldPath{ {"metadata", "ownerReferences"}, {"metadata", "selfLink"}, {"metadata", "uid"}, + {"metadata", "creationTimestamp"}, + {"metadata", "deletionTimestamp"}, + {"metadata", "resourceVersion"}, {"type"}, {"data", "tls.crt"}, @@ -23,7 +29,16 @@ var SecretSelectedFields = []FieldPath{ } // RouteSelectedFields is the list of fields sent from OpenShift Route objects to the -// backend +// backend. +// The Route resource is redacted because it may contain private keys for TLS. +// +// TODO(wallrj): Find out if the `.tls.key` field is the only one that may +// contain sensitive data and if so, that field could be redacted instead +// selecting everything else, for consistency with Ingress or any of the other +// resources that are collected. Or alternatively add an comment to explain why +// for Route, the set of fields is allow-listed while for Ingress, all fields +// are collected. +// https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/network_apis/route-route-openshift-io-v1#spec-tls-3 var RouteSelectedFields = []FieldPath{ {"kind"}, {"apiVersion"}, @@ -33,6 +48,9 @@ var RouteSelectedFields = []FieldPath{ {"metadata", "ownerReferences"}, {"metadata", "selfLink"}, {"metadata", "uid"}, + {"metadata", "creationTimestamp"}, + {"metadata", "deletionTimestamp"}, + {"metadata", "resourceVersion"}, {"spec", "host"}, {"spec", "to", "kind"}, diff --git a/pkg/datagatherer/k8s/fieldfilter_test.go b/pkg/datagatherer/k8s/fieldfilter_test.go index e5ee1e9c..a76a0365 100644 --- a/pkg/datagatherer/k8s/fieldfilter_test.go +++ b/pkg/datagatherer/k8s/fieldfilter_test.go @@ -25,6 +25,13 @@ func TestSelect(t *testing.T) { "labels": map[string]interface{}{ "foo": "bar", }, + "resourceVersion": "fake-resource-version", + "creationTimestamp": "2025-08-15T00:00:01Z", + "deletionTimestamp": "2025-08-15T00:00:02Z", + // Examples of fields which are dropped + "deletionGracePeriodSeconds": 10, + "finalizers": []string{"example.com/fake-finalizer"}, + "generation": 11, }, "type": "kubernetes.io/tls", "data": map[string]interface{}{ @@ -47,6 +54,9 @@ func TestSelect(t *testing.T) { "labels": map[string]interface{}{ "foo": "bar", }, + "resourceVersion": "fake-resource-version", + "creationTimestamp": "2025-08-15T00:00:01Z", + "deletionTimestamp": "2025-08-15T00:00:02Z", }, "type": "kubernetes.io/tls", "data": map[string]interface{}{ @@ -68,6 +78,13 @@ func TestSelect(t *testing.T) { "labels": map[string]interface{}{ "foo": "bar", }, + "resourceVersion": "fake-resource-version", + "creationTimestamp": "2025-08-15T00:00:01Z", + "deletionTimestamp": "2025-08-15T00:00:02Z", + // Examples of fields which are dropped + "deletionGracePeriodSeconds": 10, + "finalizers": []string{"example.com/fake-finalizer"}, + "generation": 11, }, "spec": map[string]interface{}{ "host": "www.example.com", @@ -94,6 +111,9 @@ func TestSelect(t *testing.T) { // "Select". "Redact" removes it. "kubectl.kubernetes.io/last-applied-configuration": "secret", }, + "resourceVersion": "fake-resource-version", + "creationTimestamp": "2025-08-15T00:00:01Z", + "deletionTimestamp": "2025-08-15T00:00:02Z", }, "spec": map[string]interface{}{ "host": "www.example.com",