From 39b34eb424c070394e0a863cb2d9f0dae51e9512 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:17:07 +0100 Subject: [PATCH 1/4] upload OIDC discovery data to disco backend Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- api/datareading.go | 1 + internal/cyberark/dataupload/dataupload.go | 9 +++++++++ pkg/client/client_cyberark.go | 20 ++++++++++++++++++++ pkg/datagatherer/oidc/oidc.go | 2 +- pkg/datagatherer/oidc/oidc_test.go | 4 ++-- 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/api/datareading.go b/api/datareading.go index 3412c847..3ea95b3f 100644 --- a/api/datareading.go +++ b/api/datareading.go @@ -64,6 +64,7 @@ func (o *DataReading) UnmarshalJSON(data []byte) error { target any assign func(any) }{ + {&OIDCDiscoveryData{}, func(v any) { o.Data = v.(*OIDCDiscoveryData) }}, {&DiscoveryData{}, func(v any) { o.Data = v.(*DiscoveryData) }}, {&DynamicData{}, func(v any) { o.Data = v.(*DynamicData) }}, } diff --git a/internal/cyberark/dataupload/dataupload.go b/internal/cyberark/dataupload/dataupload.go index b9ccb5f5..0221bf21 100644 --- a/internal/cyberark/dataupload/dataupload.go +++ b/internal/cyberark/dataupload/dataupload.go @@ -57,6 +57,15 @@ type Snapshot struct { ClusterDescription string `json:"cluster_description,omitempty"` // K8SVersion is the version of Kubernetes which the cluster is running. K8SVersion string `json:"k8s_version"` + // OIDCConfig contains OIDC configuration data from the API server's + // `/.well-known/openid-configuration` endpoint + OIDCConfig map[string]any `json:"openid_configuration,omitempty"` + // OIDCConfigError contains any error encountered while fetching the OIDC configuration + OIDCConfigError string `json:"openid_configuration_error,omitempty"` + // JWKS contains JWKS data from the API server's `/openid/v1/jwks` endpoint + JWKS map[string]any `json:"jwks,omitempty"` + // JWKSError contains any error encountered while fetching the JWKS + JWKSError string `json:"jwks_error,omitempty"` // Secrets is a list of Secret resources in the cluster. Not all Secret // types are included and only a subset of the Secret data is included. Secrets []runtime.Object `json:"secrets"` diff --git a/pkg/client/client_cyberark.go b/pkg/client/client_cyberark.go index e03fdf48..22db7d5c 100644 --- a/pkg/client/client_cyberark.go +++ b/pkg/client/client_cyberark.go @@ -104,6 +104,25 @@ func baseSnapshotFromOptions(opts Options) dataupload.Snapshot { } } +// extractOIDCFromReading converts the opaque data from a OIDCDiscoveryData +// data reading to allow access to the OIDC fields within. +func extractOIDCFromReading(reading *api.DataReading, target *dataupload.Snapshot) error { + if reading == nil { + return fmt.Errorf("programmer mistake: the DataReading must not be nil") + } + data, ok := reading.Data.(*api.OIDCDiscoveryData) + if !ok { + return fmt.Errorf( + "programmer mistake: the DataReading must have data type *api.OIDCDiscoveryData. "+ + "This DataReading (%s) has data type %T", reading.DataGatherer, reading.Data) + } + target.OIDCConfig = data.OIDCConfig + target.OIDCConfigError = data.OIDCConfigError + target.JWKS = data.JWKS + target.JWKSError = data.JWKSError + return nil +} + // extractClusterIDAndServerVersionFromReading converts the opaque data from a DiscoveryData // data reading to allow access to the Kubernetes version fields within. func extractClusterIDAndServerVersionFromReading(reading *api.DataReading, target *dataupload.Snapshot) error { @@ -161,6 +180,7 @@ func extractResourceListFromReading(reading *api.DataReading, target *[]runtime. // and populates the relevant field(s) of the Snapshot based on the DataReading's data. // Deleted resources are excluded from the snapshot because they are not needed by CyberArk. var defaultExtractorFunctions = map[string]func(*api.DataReading, *dataupload.Snapshot) error{ + "ark/oidc": extractOIDCFromReading, "ark/discovery": extractClusterIDAndServerVersionFromReading, "ark/secrets": func(r *api.DataReading, s *dataupload.Snapshot) error { return extractResourceListFromReading(r, &s.Secrets) diff --git a/pkg/datagatherer/oidc/oidc.go b/pkg/datagatherer/oidc/oidc.go index 9df3c370..5014194c 100644 --- a/pkg/datagatherer/oidc/oidc.go +++ b/pkg/datagatherer/oidc/oidc.go @@ -74,7 +74,7 @@ func (g *DataGathererOIDC) Fetch() (any, int, error) { return "" } - return api.OIDCDiscoveryData{ + return &api.OIDCDiscoveryData{ OIDCConfig: oidcResponse, OIDCConfigError: errToString(oidcErr), JWKS: jwksResponse, diff --git a/pkg/datagatherer/oidc/oidc_test.go b/pkg/datagatherer/oidc/oidc_test.go index 3c3f61f6..eda579a1 100644 --- a/pkg/datagatherer/oidc/oidc_test.go +++ b/pkg/datagatherer/oidc/oidc_test.go @@ -57,7 +57,7 @@ func TestFetch_Success(t *testing.T) { t.Fatalf("expected count 1, got %d", count) } - res, ok := anyRes.(api.OIDCDiscoveryData) + res, ok := anyRes.(*api.OIDCDiscoveryData) if !ok { t.Fatalf("unexpected result type: %T", anyRes) } @@ -101,7 +101,7 @@ func TestFetch_Errors(t *testing.T) { t.Fatalf("Fetch returned error: %v", err) } - res, ok := anyRes.(api.OIDCDiscoveryData) + res, ok := anyRes.(*api.OIDCDiscoveryData) if !ok { t.Fatalf("unexpected result type: %T", anyRes) } From 435db51afaf61779b887cc7e613434fbfb142e00 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:01:30 +0100 Subject: [PATCH 2/4] configure ark/oidc in chart and tests Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- .../disco-agent/templates/configmap.yaml | 2 ++ .../__snapshot__/configmap_test.yaml.snap | 8 +++++ examples/machinehub.yaml | 4 +++ examples/machinehub/input.json | 30 +++++++++++++++++++ pkg/client/client_cyberark_test.go | 7 +++++ 5 files changed, 51 insertions(+) diff --git a/deploy/charts/disco-agent/templates/configmap.yaml b/deploy/charts/disco-agent/templates/configmap.yaml index 231a26cd..4766e762 100644 --- a/deploy/charts/disco-agent/templates/configmap.yaml +++ b/deploy/charts/disco-agent/templates/configmap.yaml @@ -19,6 +19,8 @@ data: {{- . | toYaml | nindent 6 }} {{- end }} data-gatherers: + - kind: oidc + name: ark/oidc - kind: k8s-discovery name: ark/discovery - kind: k8s-dynamic diff --git a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap index 2c70df00..89a88ed3 100644 --- a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap +++ b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap @@ -7,6 +7,8 @@ custom-cluster-description: cluster_description: "A cloud hosted Kubernetes cluster hosting production workloads.\n\nteam: team-1\nemail: team-1@example.com\npurpose: Production workloads\n" period: "12h0m0s" data-gatherers: + - kind: oidc + name: ark/oidc - kind: k8s-discovery name: ark/discovery - kind: k8s-dynamic @@ -114,6 +116,8 @@ custom-cluster-name: cluster_description: "" period: "12h0m0s" data-gatherers: + - kind: oidc + name: ark/oidc - kind: k8s-discovery name: ark/discovery - kind: k8s-dynamic @@ -221,6 +225,8 @@ custom-period: cluster_description: "" period: "1m" data-gatherers: + - kind: oidc + name: ark/oidc - kind: k8s-discovery name: ark/discovery - kind: k8s-dynamic @@ -328,6 +334,8 @@ defaults: cluster_description: "" period: "12h0m0s" data-gatherers: + - kind: oidc + name: ark/oidc - kind: k8s-discovery name: ark/discovery - kind: k8s-dynamic diff --git a/examples/machinehub.yaml b/examples/machinehub.yaml index ea0b28e5..8845c2c3 100644 --- a/examples/machinehub.yaml +++ b/examples/machinehub.yaml @@ -12,6 +12,10 @@ # go run . agent --one-shot --machine-hub -v 6 --agent-config-file ./examples/machinehub.yaml data-gatherers: +# Gather Kubernetes OIDC information +- name: ark/oidc + kind: oidc + # Gather Kubernetes API server version information - name: ark/discovery kind: k8s-discovery diff --git a/examples/machinehub/input.json b/examples/machinehub/input.json index 2cdba65c..21564538 100644 --- a/examples/machinehub/input.json +++ b/examples/machinehub/input.json @@ -1,4 +1,34 @@ [ + { + "data-gatherer": "ark/oidc", + "data": { + "openid_configuration": { + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "issuer": "https://kubernetes.default.svc.cluster.local", + "jwks_uri": "https://10.10.1.2:6443/openid/v1/jwks", + "response_types_supported": [ + "id_token" + ], + "subject_types_supported": [ + "public" + ] + }, + "jwks": { + "keys": [ + { + "alg": "RS256", + "e": "AQAB", + "kid": "C-2916LkMJqepqULK2nqhq6uzVB6So_yyGnqyuor71Q", + "kty": "RSA", + "n": "sYh6rDpl5DyzBk8qlnYXo6Sf9WbplnXJv3tPxWTvhCFsVu9G5oWjknkafVDq5UOJrlybJJNjBmUyiEi1wbdnuhceJS7rZ3sRnNp3aNoS0omCR6iHJCOuoboSlcaPuRmYw4oWXlVUXlKyw8PYPVbNCcTLuq9nqf8y33mIqe7XJsf5-Z5P05WbK9Rzj-SJvlZLQ4dSFtIiwqLkm_2fpRLj0d8Af1F6vuztnhhUE2_PDsfIWdl_kJKkrK3B5x7k5tgTyFrNQPzlRBgK9jmK0HskwAFIDaLKb7FUWuUiQjn94rjKCED4iy201YPAoZBKIHFDlFVkQ_S3quwPcRyOS18r7w", + "use": "sig" + } + ] + } + } + }, { "data-gatherer": "ark/discovery", "data": { diff --git a/pkg/client/client_cyberark_test.go b/pkg/client/client_cyberark_test.go index 61c33764..1ed1caed 100644 --- a/pkg/client/client_cyberark_test.go +++ b/pkg/client/client_cyberark_test.go @@ -104,6 +104,13 @@ func fakeReadings() []*api.DataReading { } return append([]*api.DataReading{ + { + DataGatherer: "ark/oidc", + Data: &api.OIDCDiscoveryData{ + OIDCConfigError: "Failed to fetch /.well-known/openid-configuration: 404 Not Found", + JWKSError: "Failed to fetch /openid/v1/jwks: 404 Not Found", + }, + }, { DataGatherer: "ark/discovery", Data: &api.DiscoveryData{ From 85399434b78e63aedc757b92a242698866fb3d64 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:38:35 +0100 Subject: [PATCH 3/4] add unit tests Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- api/datareading_test.go | 14 ++++ ...lient_cyberark_convertdatareadings_test.go | 64 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/api/datareading_test.go b/api/datareading_test.go index 9fa90b01..087877d8 100644 --- a/api/datareading_test.go +++ b/api/datareading_test.go @@ -75,6 +75,20 @@ func TestDataReading_UnmarshalJSON(t *testing.T) { }`, wantDataType: &DynamicData{}, }, + { + name: "OIDCDiscoveryData type", + input: `{ + "cluster_id": "11111111-2222-3333-4444-555555555555", + "data-gatherer": "oidc", + "timestamp": "2024-06-01T12:00:00Z", + "data": { + "openid_configuration": {"issuer": "https://example.com"}, + "jwks": {"keys": []} + }, + "schema_version": "v1" + }`, + wantDataType: &OIDCDiscoveryData{}, + }, { name: "Invalid JSON", input: `not a json`, diff --git a/pkg/client/client_cyberark_convertdatareadings_test.go b/pkg/client/client_cyberark_convertdatareadings_test.go index 4fc33198..675c5bb6 100644 --- a/pkg/client/client_cyberark_convertdatareadings_test.go +++ b/pkg/client/client_cyberark_convertdatareadings_test.go @@ -126,6 +126,70 @@ func TestExtractServerVersionFromReading(t *testing.T) { } } +// TestExtractOIDCFromReading tests the extractOIDCFromReading function. +func TestExtractOIDCFromReading(t *testing.T) { + type testCase struct { + name string + reading *api.DataReading + expectedSnapshot dataupload.Snapshot + expectError string + } + tests := []testCase{ + { + name: "nil reading", + expectError: `programmer mistake: the DataReading must not be nil`, + }, + { + name: "nil data", + reading: &api.DataReading{ + DataGatherer: "ark/oidc", + Data: nil, + }, + expectError: `programmer mistake: the DataReading must have data type *api.OIDCDiscoveryData. This DataReading (ark/oidc) has data type `, + }, + { + name: "wrong data type", + reading: &api.DataReading{ + DataGatherer: "ark/oidc", + Data: &api.DiscoveryData{}, + }, + expectError: `programmer mistake: the DataReading must have data type *api.OIDCDiscoveryData. This DataReading (ark/oidc) has data type *api.DiscoveryData`, + }, + { + name: "happy path", + reading: &api.DataReading{ + DataGatherer: "ark/oidc", + Data: &api.OIDCDiscoveryData{ + OIDCConfig: map[string]any{"issuer": "https://example.com"}, + OIDCConfigError: "oidc-err", + JWKS: map[string]any{"keys": []any{}}, + JWKSError: "jwks-err", + }, + }, + expectedSnapshot: dataupload.Snapshot{ + OIDCConfig: map[string]any{"issuer": "https://example.com"}, + OIDCConfigError: "oidc-err", + JWKS: map[string]any{"keys": []any{}}, + JWKSError: "jwks-err", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var snapshot dataupload.Snapshot + err := extractOIDCFromReading(test.reading, &snapshot) + if test.expectError != "" { + assert.EqualError(t, err, test.expectError) + assert.Equal(t, dataupload.Snapshot{}, snapshot) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedSnapshot, snapshot) + }) + } +} + // TestExtractResourceListFromReading tests the extractResourceListFromReading function. func TestExtractResourceListFromReading(t *testing.T) { type testCase struct { From d155d1c6b31cca56075c01fa776377f976117cc5 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:48:49 +0100 Subject: [PATCH 4/4] improve k8s error messages Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- pkg/datagatherer/oidc/oidc.go | 41 ++++++++++++++++++++++++++++-- pkg/datagatherer/oidc/oidc_test.go | 2 +- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/pkg/datagatherer/oidc/oidc.go b/pkg/datagatherer/oidc/oidc.go index 5014194c..75a7a7aa 100644 --- a/pkg/datagatherer/oidc/oidc.go +++ b/pkg/datagatherer/oidc/oidc.go @@ -4,8 +4,13 @@ import ( "context" "encoding/json" "fmt" + "net/url" + "strings" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/rest" + "k8s.io/klog/v2" "github.com/jetstack/preflight/api" "github.com/jetstack/preflight/pkg/datagatherer" @@ -86,7 +91,7 @@ func (g *DataGathererOIDC) fetchOIDCConfig(ctx context.Context) (map[string]any, // Fetch the OIDC discovery document from the well-known endpoint. bytes, err := g.cl.Get().AbsPath("/.well-known/openid-configuration").Do(ctx).Raw() if err != nil { - return nil, fmt.Errorf("failed to get OIDC discovery document: %v", err) + return nil, fmt.Errorf("failed to get OIDC discovery document: %s", k8sErrorMessage(err)) } var oidcResponse map[string]any @@ -106,7 +111,7 @@ func (g *DataGathererOIDC) fetchJWKS(ctx context.Context) (map[string]any, error // So we are using the default path instead, which we think should work in most cases. bytes, err := g.cl.Get().AbsPath("/openid/v1/jwks").Do(ctx).Raw() if err != nil { - return nil, fmt.Errorf("failed to get JWKS from jwks_uri: %v", err) + return nil, fmt.Errorf("failed to get JWKS from jwks_uri: %s", k8sErrorMessage(err)) } var jwksResponse map[string]any @@ -116,3 +121,35 @@ func (g *DataGathererOIDC) fetchJWKS(ctx context.Context) (map[string]any, error return jwksResponse, nil } + +// based on https://github.com/kubernetes/kubectl/blob/a64ceaeab69eed1f11a9e1bd91cf2c1446de811c/pkg/cmd/util/helpers.go#L244 +func k8sErrorMessage(err error) string { + if status, isStatus := err.(apierrors.APIStatus); isStatus { + switch s := status.Status(); { + case s.Reason == metav1.StatusReasonUnauthorized: + return fmt.Sprintf("error: You must be logged in to the server (%s)", s.Message) + case len(s.Reason) > 0: + return fmt.Sprintf("Error from server (%s): %s", s.Reason, err.Error()) + default: + return fmt.Sprintf("Error from server: %s", err.Error()) + } + } + + if apierrors.IsUnexpectedObjectError(err) { + return fmt.Sprintf("Server returned an unexpected response: %s", err.Error()) + } + + if t, isURL := err.(*url.Error); isURL { + klog.V(4).Infof("Connection error: %s %s: %v", t.Op, t.URL, t.Err) + if strings.Contains(t.Err.Error(), "connection refused") { + host := t.URL + if server, err := url.Parse(t.URL); err == nil { + host = server.Host + } + return fmt.Sprintf("The connection to the server %s was refused - did you specify the right host or port?", host) + } + return fmt.Sprintf("Unable to connect to the server: %v", t.Err) + } + + return fmt.Sprintf("error: %v", err) +} diff --git a/pkg/datagatherer/oidc/oidc_test.go b/pkg/datagatherer/oidc/oidc_test.go index eda579a1..e0fc6c17 100644 --- a/pkg/datagatherer/oidc/oidc_test.go +++ b/pkg/datagatherer/oidc/oidc_test.go @@ -109,7 +109,7 @@ func TestFetch_Errors(t *testing.T) { if res.OIDCConfig != nil { t.Fatalf("expected nil OIDCConfig on error, got %#v", res.OIDCConfig) } - if res.OIDCConfigError != "failed to get OIDC discovery document: an error on the server (\"boom\") has prevented the request from succeeding" { + if res.OIDCConfigError != "failed to get OIDC discovery document: Error from server (InternalError): an error on the server (\"boom\") has prevented the request from succeeding" { t.Fatalf("unexpected OIDCConfigError: %q", res.OIDCConfigError) } if res.JWKS != nil {