Skip to content
Open
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
1 change: 1 addition & 0 deletions api/datareading.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) }},
}
Expand Down
14 changes: 14 additions & 0 deletions api/datareading_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
2 changes: 2 additions & 0 deletions deploy/charts/disco-agent/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions examples/machinehub.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions examples/machinehub/input.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
9 changes: 9 additions & 0 deletions internal/cyberark/dataupload/dataupload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
20 changes: 20 additions & 0 deletions pkg/client/client_cyberark.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
64 changes: 64 additions & 0 deletions pkg/client/client_cyberark_convertdatareadings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <nil>`,
},
{
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 {
Expand Down
7 changes: 7 additions & 0 deletions pkg/client/client_cyberark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
74 changes: 63 additions & 11 deletions pkg/datagatherer/oidc/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@ 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"
"github.com/jetstack/preflight/pkg/kubeconfig"
)

// OIDCDiscovery contains the configuration for the k8s-discovery data-gatherer
// OIDCDiscovery contains the configuration for the oidc data-gatherer.
type OIDCDiscovery struct {
// KubeConfigPath is the path to the kubeconfig file. If empty, will assume it runs in-cluster.
KubeConfigPath string `yaml:"kubeconfig"`
Expand Down Expand Up @@ -44,7 +49,7 @@ func (c *OIDCDiscovery) NewDataGatherer(ctx context.Context) (datagatherer.DataG
}, nil
}

// DataGathererOIDC stores the config for a k8s-discovery datagatherer
// DataGathererOIDC stores the config for an oidc datagatherer.
type DataGathererOIDC struct {
cl rest.Interface
}
Expand Down Expand Up @@ -74,7 +79,14 @@ func (g *DataGathererOIDC) Fetch() (any, int, error) {
return ""
}

return api.OIDCDiscoveryData{
if oidcErr != nil {
klog.FromContext(ctx).V(4).Error(oidcErr, "Failed to fetch OIDC configuration")
}
if jwksErr != nil {
klog.FromContext(ctx).V(4).Error(jwksErr, "Failed to fetch JWKS")
}

return &api.OIDCDiscoveryData{
OIDCConfig: oidcResponse,
OIDCConfigError: errToString(oidcErr),
JWKS: jwksResponse,
Expand All @@ -84,14 +96,15 @@ func (g *DataGathererOIDC) Fetch() (any, int, error) {

func (g *DataGathererOIDC) fetchOIDCConfig(ctx context.Context) (map[string]any, error) {
// 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)
result := g.cl.Get().AbsPath("/.well-known/openid-configuration").Do(ctx)
if err := result.Error(); err != nil {
return nil, fmt.Errorf("failed to get /.well-known/openid-configuration: %s", k8sErrorMessage(err))
}

bytes, _ := result.Raw() // we already checked result.Error(), so there is no error here
var oidcResponse map[string]any
if err := json.Unmarshal(bytes, &oidcResponse); err != nil {
return nil, fmt.Errorf("failed to unmarshal OIDC discovery document: %v", err)
return nil, fmt.Errorf("failed to unmarshal OIDC discovery document: %v (raw: %q)", err, stringFirstN(string(bytes), 80))
}

return oidcResponse, nil
Expand All @@ -104,15 +117,54 @@ func (g *DataGathererOIDC) fetchJWKS(ctx context.Context) (map[string]any, error
// - on fully private AWS EKS clusters, the URL is still public and might not
// be reachable from within the cluster (https://github.com/aws/containers-roadmap/issues/2038)
// 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)
result := g.cl.Get().AbsPath("/openid/v1/jwks").Do(ctx)
if err := result.Error(); err != nil {
return nil, fmt.Errorf("failed to get /openid/v1/jwks: %s", k8sErrorMessage(err))
}

bytes, _ := result.Raw() // we already checked result.Error(), so there is no error here
var jwksResponse map[string]any
if err := json.Unmarshal(bytes, &jwksResponse); err != nil {
return nil, fmt.Errorf("failed to unmarshal JWKS response: %v", err)
return nil, fmt.Errorf("failed to unmarshal JWKS response: %v (raw: %q)", err, stringFirstN(string(bytes), 80))
}

return jwksResponse, nil
}

func stringFirstN(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n]
}

// 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 {
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)
}
Loading