From a683164e2f69e87b23be343b7af7134284f62f04 Mon Sep 17 00:00:00 2001 From: jpaodev Date: Fri, 13 Feb 2026 16:14:48 +0100 Subject: [PATCH 1/9] feat: add MondooOperatorConfig for proxy and image registry support Introduce MondooOperatorConfig CRD with support for: - HTTP/HTTPS proxy configuration (httpProxy, httpsProxy, noProxy) - Container proxy for image scanning - Image pull secrets for private registries - Image registry mirror support - Registry mirrors mapping - Skip proxy for cnspec option Tested on GKE Autopilot successfully. --- api/v1alpha2/mondoooperatorconfig_types.go | 25 + api/v1alpha2/zz_generated.deepcopy.go | 27 + .../k8s.mondoo.com_mondooauditconfigs.yaml | 1302 +++++++++++++++++ .../k8s.mondoo.com_mondoooperatorconfigs.yaml | 169 +++ .../mondoo-operator/templates/deployment.yaml | 4 + .../templates/mondooauditconfig-crd.yaml | 1249 ---------------- .../templates/mondoooperatorconfig-crd.yaml | 104 -- .../templates/mondoooperatorconfig.yaml | 38 + charts/mondoo-operator/values.yaml | 40 + .../k8s.mondoo.com_mondoooperatorconfigs.yaml | 55 + controllers/container_image/resources.go | 26 +- .../integration/integration_controller.go | 2 +- controllers/k8s_scan/resources.go | 31 +- controllers/mondooauditconfig_controller.go | 23 +- controllers/nodes/resources.go | 48 +- controllers/status/status_reporter.go | 1 + pkg/client/common/http.go | 66 +- pkg/client/mondooclient/client.go | 3 +- pkg/imagecache/imagecache.go | 130 +- pkg/utils/mondoo/container_image_resolver.go | 140 +- .../mondoo/container_image_resolver_test.go | 7 + .../mondoo/fake/container_image_resolver.go | 14 + pkg/utils/mondoo/integration.go | 2 + pkg/utils/mondoo/token_exchange.go | 9 +- 24 files changed, 2134 insertions(+), 1381 deletions(-) create mode 100644 charts/mondoo-operator/crds/k8s.mondoo.com_mondooauditconfigs.yaml create mode 100644 charts/mondoo-operator/crds/k8s.mondoo.com_mondoooperatorconfigs.yaml delete mode 100644 charts/mondoo-operator/templates/mondooauditconfig-crd.yaml delete mode 100644 charts/mondoo-operator/templates/mondoooperatorconfig-crd.yaml create mode 100644 charts/mondoo-operator/templates/mondoooperatorconfig.yaml diff --git a/api/v1alpha2/mondoooperatorconfig_types.go b/api/v1alpha2/mondoooperatorconfig_types.go index 5909737b3..b254f0f84 100644 --- a/api/v1alpha2/mondoooperatorconfig_types.go +++ b/api/v1alpha2/mondoooperatorconfig_types.go @@ -28,8 +28,33 @@ type MondooOperatorConfigSpec struct { SkipContainerResolution bool `json:"skipContainerResolution,omitempty"` // HttpProxy specifies a proxy to use for HTTP requests to the Mondoo Platform. HttpProxy *string `json:"httpProxy,omitempty"` + // HttpsProxy specifies a proxy to use for HTTPS requests to the Mondoo Platform. + HttpsProxy *string `json:"httpsProxy,omitempty"` + // NoProxy specifies a comma-separated list of hosts that should not use the proxy. + NoProxy *string `json:"noProxy,omitempty"` // ContainerProxy specifies a proxy to use for container images. ContainerProxy *string `json:"containerProxy,omitempty"` + // ImagePullSecrets specifies the name of the Secret to use for pulling images for all Mondoo components. + // The secret must be of type kubernetes.io/dockerconfigjson. + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + // ImageRegistry specifies a custom container image registry to use for all Mondoo images. + // This allows using a private registry mirror (e.g., artifactory.example.com/ghcr.io.docker). + // If set, all image references will be prefixed with this registry. + // Deprecated: Use RegistryMirrors for more flexible registry mapping. + ImageRegistry *string `json:"imageRegistry,omitempty"` + // RegistryMirrors specifies a mapping of public registries to private mirrors. + // The key is the public registry (e.g., "ghcr.io", "docker.io", "quay.io") + // and the value is the private mirror (e.g., "artifactory.example.com/ghcr.io.docker"). + // Example: + // registryMirrors: + // ghcr.io: artifactory.example.com/ghcr.io.docker + // docker.io: artifactory.example.com/hub.docker.com + RegistryMirrors map[string]string `json:"registryMirrors,omitempty"` + // SkipProxyForCnspec disables proxy environment variables for cnspec-based components + // (scan-api, container scanning). Use this when the Mondoo API is accessible directly + // without proxy (e.g., internal mirror) but other components need proxy for external access. + // Default: false (proxy settings are applied to all components) + SkipProxyForCnspec bool `json:"skipProxyForCnspec,omitempty"` } type Metrics struct { diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index f94488396..32cc0b08a 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -514,11 +514,38 @@ func (in *MondooOperatorConfigSpec) DeepCopyInto(out *MondooOperatorConfigSpec) *out = new(string) **out = **in } + if in.HttpsProxy != nil { + in, out := &in.HttpsProxy, &out.HttpsProxy + *out = new(string) + **out = **in + } + if in.NoProxy != nil { + in, out := &in.NoProxy, &out.NoProxy + *out = new(string) + **out = **in + } if in.ContainerProxy != nil { in, out := &in.ContainerProxy, &out.ContainerProxy *out = new(string) **out = **in } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } + if in.ImageRegistry != nil { + in, out := &in.ImageRegistry, &out.ImageRegistry + *out = new(string) + **out = **in + } + if in.RegistryMirrors != nil { + in, out := &in.RegistryMirrors, &out.RegistryMirrors + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MondooOperatorConfigSpec. diff --git a/charts/mondoo-operator/crds/k8s.mondoo.com_mondooauditconfigs.yaml b/charts/mondoo-operator/crds/k8s.mondoo.com_mondooauditconfigs.yaml new file mode 100644 index 000000000..bc05d9776 --- /dev/null +++ b/charts/mondoo-operator/crds/k8s.mondoo.com_mondooauditconfigs.yaml @@ -0,0 +1,1302 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: mondooauditconfigs.k8s.mondoo.com +spec: + group: k8s.mondoo.com + names: + kind: MondooAuditConfig + listKind: MondooAuditConfigList + plural: mondooauditconfigs + singular: mondooauditconfig + scope: Namespaced + versions: + - name: v1alpha2 + schema: + openAPIV3Schema: + description: MondooAuditConfig is the Schema for the mondooauditconfigs API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: MondooAuditConfigSpec defines the desired state of MondooAuditConfig + properties: + admission: + description: |- + Admission is DEPRECATED and ignored. Admission webhooks were removed in v12.1.0. + The operator will automatically clean up any orphaned admission resources. + See docs/admission-migration-guide.md for migration instructions. + properties: + certificateProvisioning: + description: CertificateProvisioning is DEPRECATED. + properties: + mode: + description: Mode is DEPRECATED. + type: string + type: object + enable: + description: Enable is DEPRECATED. Admission webhooks are no longer + supported. + type: boolean + image: + description: Image is DEPRECATED. + properties: + digest: + description: |- + Digest specifies the image digest (e.g., sha256:abc123...). + When specified, this takes precedence over Tag. + type: string + name: + type: string + tag: + type: string + type: object + mode: + description: Mode is DEPRECATED. + type: string + replicas: + description: Replicas is DEPRECATED. + format: int32 + type: integer + serviceAccountName: + description: ServiceAccountName is DEPRECATED. + type: string + type: object + annotations: + additionalProperties: + type: string + description: |- + Annotations allows adding custom annotations to all scanned assets. These key-value pairs + will be attached to every asset discovered by the operator, making them searchable + and filterable in the Mondoo Console. + type: object + consoleIntegration: + properties: + enable: + type: boolean + type: object + containers: + properties: + enable: + type: boolean + env: + description: |- + Env allows setting extra environment variables for the node scanner. If the operator sets already an env + variable with the same name, the value specified here will override it. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + schedule: + description: Specify a custom crontab schedule for the container + image scanning job. If not specified, the default schedule is + used. + type: string + type: object + filtering: + properties: + namespaces: + properties: + exclude: + description: |- + Exclude is the list of resources to ignore for any watching/scanning actions. Use this if + the goal is to watch/scan all resources except for this Exclude list. + items: + type: string + type: array + include: + description: |- + Include is the list of resources to watch/scan. Setting Include overrides anything in the + Exclude list as specifying an Include list is effectively excluding everything except for what + is on the Include list. + items: + type: string + type: array + type: object + type: object + kubernetesResources: + properties: + containerImageScanning: + description: |- + DEPRECATED: ContainerImageScanning determines whether container images are being scanned. The current implementation + runs a separate job once every 24h that scans the container images running in the cluster. + type: boolean + enable: + type: boolean + externalClusters: + description: |- + ExternalClusters defines remote K8s clusters to scan from this operator instance. + Each external cluster will have its own CronJob created with the appropriate kubeconfig. + items: + description: ExternalCluster defines configuration for scanning + a remote K8s cluster + properties: + containerImageScanning: + description: ContainerImageScanning enables scanning of + container images in this external cluster. + type: boolean + filtering: + description: |- + Filtering allows namespace filtering specific to this external cluster. + If not specified, uses the global filtering from MondooAuditConfigSpec.Filtering. + properties: + namespaces: + properties: + exclude: + description: |- + Exclude is the list of resources to ignore for any watching/scanning actions. Use this if + the goal is to watch/scan all resources except for this Exclude list. + items: + type: string + type: array + include: + description: |- + Include is the list of resources to watch/scan. Setting Include overrides anything in the + Exclude list as specifying an Include list is effectively excluding everything except for what + is on the Include list. + items: + type: string + type: array + type: object + type: object + kubeconfigSecretRef: + description: |- + KubeconfigSecretRef references a Secret containing kubeconfig for the remote cluster. + The Secret must have a key "kubeconfig" with the kubeconfig content. + Mutually exclusive with ServiceAccountAuth and WorkloadIdentity. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Name is a unique identifier for this cluster (used in resource names). + Must be a valid Kubernetes name (lowercase, alphanumeric, hyphens allowed). + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + privateRegistriesPullSecretRef: + description: |- + PrivateRegistriesPullSecretRef references a Secret containing registry credentials + for pulling/scanning private images in this remote cluster. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + schedule: + description: |- + Schedule overrides the default schedule for this cluster (optional). + If not specified, uses the schedule from KubernetesResources.Schedule. + type: string + serviceAccountAuth: + description: |- + ServiceAccountAuth configures authentication using a service account token. + Mutually exclusive with KubeconfigSecretRef and WorkloadIdentity. + properties: + credentialsSecretRef: + description: |- + CredentialsSecretRef references a Secret containing: + - "token": The service account token (required) + - "ca.crt": The CA certificate (required) + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + server: + description: |- + Server is the URL of the Kubernetes API server. + Example: "https://my-cluster.example.com:6443" + pattern: ^https://.* + type: string + skipTLSVerify: + default: false + description: SkipTLSVerify skips TLS verification. NOT + RECOMMENDED for production. + type: boolean + required: + - credentialsSecretRef + - server + type: object + spiffeAuth: + description: |- + SPIFFEAuth configures SPIFFE/SPIRE-based authentication using X.509 SVIDs. + Mutually exclusive with KubeconfigSecretRef, ServiceAccountAuth, and WorkloadIdentity. + properties: + audience: + description: |- + Audience is the intended audience for the SVID (optional). + Some SPIRE configurations require this for workload attestation. + type: string + server: + description: |- + Server is the URL of the remote Kubernetes API server. + Example: "https://remote-cluster.example.com:6443" + pattern: ^https://.* + type: string + socketPath: + default: /run/spire/sockets/agent.sock + description: |- + SocketPath is the path to the SPIRE agent's Workload API socket. + Defaults to "/run/spire/sockets/agent.sock" if not specified. + type: string + trustBundleSecretRef: + description: |- + TrustBundleSecretRef references a Secret containing the remote cluster's + CA certificate for TLS verification. The Secret must have a key "ca.crt". + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - server + - trustBundleSecretRef + type: object + workloadIdentity: + description: |- + WorkloadIdentity configures cloud-native Workload Identity Federation authentication. + Mutually exclusive with KubeconfigSecretRef, ServiceAccountAuth, and SPIFFEAuth. + properties: + aks: + description: |- + AKS contains AKS-specific Azure Workload Identity configuration. + Required when provider is "aks". + properties: + clientId: + description: ClientID is the Azure AD app client + ID. + type: string + clusterName: + description: ClusterName is the AKS cluster name. + type: string + resourceGroup: + description: ResourceGroup containing the AKS cluster. + type: string + subscriptionId: + description: SubscriptionID is the Azure subscription. + type: string + tenantId: + description: TenantID is the Azure AD tenant ID. + type: string + required: + - clientId + - clusterName + - resourceGroup + - subscriptionId + - tenantId + type: object + eks: + description: |- + EKS contains EKS-specific IRSA configuration. + Required when provider is "eks". + properties: + clusterName: + description: ClusterName is the EKS cluster name. + type: string + region: + description: Region is the AWS region. + type: string + roleArn: + description: |- + RoleARN is the IAM role to assume. + Format: arn:aws:iam:::role/ + type: string + required: + - clusterName + - region + - roleArn + type: object + gke: + description: |- + GKE contains GKE-specific Workload Identity configuration. + Required when provider is "gke". + properties: + clusterLocation: + description: ClusterLocation is the region or zone + (e.g., "us-central1" or "us-central1-a"). + type: string + clusterName: + description: ClusterName is the GKE cluster name. + type: string + googleServiceAccount: + description: |- + GoogleServiceAccount is the Google service account to impersonate. + Format: @.iam.gserviceaccount.com + type: string + projectId: + description: ProjectID is the GCP project ID. + type: string + required: + - clusterLocation + - clusterName + - googleServiceAccount + - projectId + type: object + provider: + description: Provider specifies the cloud provider for + WIF. + enum: + - gke + - eks + - aks + type: string + required: + - provider + type: object + required: + - name + type: object + type: array + resourceWatcher: + description: |- + ResourceWatcher configures real-time resource watching and scanning. + When enabled, a deployment will be created that watches for K8s resource changes + and scans them immediately rather than waiting for the CronJob schedule. + properties: + debounceInterval: + default: 10s + description: |- + DebounceInterval specifies how long to batch changes before triggering a scan. + This prevents excessive scanning when multiple resources change in quick succession. + Default is 10 seconds. + type: string + enable: + description: |- + Enable enables real-time resource watching and scanning. + When enabled, a deployment will be created that watches K8s resources for changes + and scans them using cnspec. + type: boolean + minimumScanInterval: + default: 2m + description: |- + MinimumScanInterval specifies the minimum time between scans (rate limit). + This provides a hard limit on scan frequency even when resources are changing continuously. + Default is 2 minutes. + type: string + resourceTypes: + description: |- + ResourceTypes specifies which resource types to watch. If not specified, defaults are used + based on WatchAllResources setting. When WatchAllResources is false (default), defaults to: + deployments, daemonsets, statefulsets, replicasets. When true, defaults to: + pods, deployments, daemonsets, statefulsets, replicasets, jobs, cronjobs, services, ingresses, namespaces + items: + type: string + type: array + watchAllResources: + description: |- + WatchAllResources controls whether to watch all resource types or only high-priority ones. + When false (default), only watches stable workload resources: Deployments, DaemonSets, + StatefulSets, and ReplicaSets. When true, watches all resources including ephemeral ones + like Pods, Jobs, and CronJobs. + type: boolean + type: object + schedule: + description: Specify a custom crontab schedule for the Kubernetes + resource scanning job. If not specified, the default schedule + is used. + type: string + type: object + mondooCredsSecretRef: + description: Config is an example field of MondooAuditConfig. Edit + mondooauditconfig_types.go to remove/update + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + mondooTokenSecretRef: + description: |- + MondooTokenSecretRef can optionally hold a time-limited token that the mondoo-operator will use + to create a Mondoo service account saved to the Secret specified in .spec.mondooCredsSecretRef + if that Secret does not exist. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + nodes: + properties: + enable: + type: boolean + env: + description: |- + Env allows setting extra environment variables for the node scanner. If the operator sets already an env + variable with the same name, the value specified here will override it. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + intervalTimer: + default: 60 + description: |- + IntervalTimer is the interval (in minutes) for the node scanning. The default is "60". Only applicable for Deployment + style. + type: integer + priorityClassName: + description: PriorityClassName specifies the name of the PriorityClass + for the node scanning workloads. + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + schedule: + description: |- + Schedule specifies a custom crontab schedule for the node scanning job. If not specified, the default schedule is + used. Only applicable for CronJob style + type: string + style: + default: cronjob + description: Style specifies how node scanning is deployed. The + default is "cronjob" which will create a CronJob for the node + scanning. + enum: + - cronjob + - deployment + - daemonset + type: string + type: object + scanner: + description: |- + Scanner defines the settings for the Mondoo scanner that will be running in the cluster. The same scanner + is used for scanning the Kubernetes API and the nodes. + properties: + env: + description: |- + Env allows setting extra environment variables for the scanner. If the operator sets already an env + variable with the same name, the value specified here will override it. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + properties: + digest: + description: |- + Digest specifies the image digest (e.g., sha256:abc123...). + When specified, this takes precedence over Tag. + type: string + name: + type: string + tag: + type: string + type: object + privateRegistriesPullSecretRef: + description: |- + PrivateRegistriesPullSecretRef defines the name of a secret that contains the credentials for the private + registries we have to pull images from. Use this when you have a single secret containing credentials + for one or more registries. For multiple separate secrets, use PrivateRegistriesPullSecretRefs instead. + Deprecated: Use PrivateRegistriesPullSecretRefs for new configurations. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + privateRegistriesPullSecretRefs: + description: |- + PrivateRegistriesPullSecretRefs defines a list of secrets that contain credentials for private + registries. Use this when you need to pull images from multiple private registries and the credentials + are stored in separate secrets (e.g., managed by different teams or external secret operators). + The credentials from all secrets will be merged. If both PrivateRegistriesPullSecretRef and + PrivateRegistriesPullSecretRefs are specified, all secrets will be merged together. + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + replicas: + default: 1 + description: |- + Number of replicas for the scanner. + For enforcing mode, the minimum should be two to prevent problems during Pod failures, + e.g. node failure, node scaling, etc. + format: int32 + minimum: 1 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + serviceAccountName: + default: mondoo-operator-k8s-resources-scanning + type: string + type: object + required: + - mondooCredsSecretRef + type: object + status: + description: MondooAuditConfigStatus defines the observed state of MondooAuditConfig + properties: + conditions: + description: Conditions includes detailed status for the MondooAuditConfig + items: + properties: + affectedPods: + description: AffectedPods, when filled, contains a list which + are affected by an issue + items: + type: string + type: array + lastTransitionTime: + description: LastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + lastUpdateTime: + description: LastUpdateTime is the last time we probed the condition + format: date-time + type: string + memoryLimit: + description: MemoryLimit contains the currently active memory + limit for a Pod + type: string + message: + description: Message is a human-readable message indicating + details about the last transition + type: string + reason: + description: Reason is a unique, one-word, CamelCase reason + for the condition's last transition + type: string + status: + description: Status is the status of the condition + type: string + type: + description: Type is the specific type of the condition + type: string + required: + - status + - type + type: object + type: array + lastK8sResourceGarbageCollectionTime: + description: |- + LastK8sResourceGarbageCollectionTime tracks the last time the operator performed + garbage collection of stale K8s resource scan assets. + format: date-time + type: string + pods: + description: Pods store the name of the pods which are running mondoo + instances + items: + type: string + type: array + reconciledByOperatorVersion: + description: ReconciledByOperatorVersion contains the version of the + operator which reconciled this MondooAuditConfig + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/mondoo-operator/crds/k8s.mondoo.com_mondoooperatorconfigs.yaml b/charts/mondoo-operator/crds/k8s.mondoo.com_mondoooperatorconfigs.yaml new file mode 100644 index 000000000..3121a0c37 --- /dev/null +++ b/charts/mondoo-operator/crds/k8s.mondoo.com_mondoooperatorconfigs.yaml @@ -0,0 +1,169 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: mondoooperatorconfigs.k8s.mondoo.com +spec: + group: k8s.mondoo.com + names: + kind: MondooOperatorConfig + listKind: MondooOperatorConfigList + plural: mondoooperatorconfigs + singular: mondoooperatorconfig + scope: Cluster + versions: + - name: v1alpha2 + schema: + openAPIV3Schema: + description: MondooOperatorConfig is the Schema for the mondoooperatorconfigs + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: MondooOperatorConfigSpec defines the desired state of MondooOperatorConfig + properties: + containerProxy: + description: ContainerProxy specifies a proxy to use for container + images. + type: string + httpProxy: + description: HttpProxy specifies a proxy to use for HTTP requests + to the Mondoo Platform. + type: string + httpsProxy: + description: HttpsProxy specifies a proxy to use for HTTPS requests + to the Mondoo Platform. + type: string + imagePullSecrets: + description: |- + ImagePullSecrets specifies the name of the Secret to use for pulling images for all Mondoo components. + The secret must be of type kubernetes.io/dockerconfigjson. + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + imageRegistry: + description: |- + ImageRegistry specifies a custom container image registry to use for all Mondoo images. + This allows using a private registry mirror (e.g., artifactory.example.com/ghcr.io.docker). + If set, all image references will be prefixed with this registry. + Deprecated: Use RegistryMirrors for more flexible registry mapping. + type: string + metrics: + description: Metrics controls the enabling/disabling of metrics report + of mondoo-operator + properties: + enable: + type: boolean + resourceLabels: + additionalProperties: + type: string + description: |- + ResourceLabels allows providing a list of extra labels to apply to the metrics-related + resources (eg. ServiceMonitor) + type: object + type: object + noProxy: + description: NoProxy specifies a comma-separated list of hosts that + should not use the proxy. + type: string + registryMirrors: + additionalProperties: + type: string + description: |- + RegistryMirrors specifies a mapping of public registries to private mirrors. + The key is the public registry (e.g., "ghcr.io", "docker.io", "quay.io") + and the value is the private mirror (e.g., "artifactory.example.com/ghcr.io.docker"). + Example: + registryMirrors: + ghcr.io: artifactory.example.com/ghcr.io.docker + docker.io: artifactory.example.com/hub.docker.com + type: object + skipContainerResolution: + description: Allows skipping Image resolution from upstream repository + type: boolean + skipProxyForCnspec: + description: |- + SkipProxyForCnspec disables proxy environment variables for cnspec-based components + (scan-api, container scanning). Use this when the Mondoo API is accessible directly + without proxy (e.g., internal mirror) but other components need proxy for external access. + Default: false (proxy settings are applied to all components) + type: boolean + type: object + status: + description: MondooOperatorConfigStatus defines the observed state of + MondooOperatorConfig + properties: + conditions: + description: Conditions includes more detailed status for the mondoo + config + items: + description: Condition contains details for the current condition + of a MondooOperatorConfig + properties: + lastTransitionTime: + description: LastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + lastUpdateTime: + description: LastUpdateTime is the last time the condition was + updated. + format: date-time + type: string + message: + description: Message is a human-readable message indicating + details about last transition. + type: string + reason: + description: Reason is a unique, one-word, CamelCase reason + for the condition's last transition. + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: Type is the type of the condition. + type: string + required: + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/mondoo-operator/templates/deployment.yaml b/charts/mondoo-operator/templates/deployment.yaml index 86bd5fa9d..293ebf6af 100644 --- a/charts/mondoo-operator/templates/deployment.yaml +++ b/charts/mondoo-operator/templates/deployment.yaml @@ -59,3 +59,7 @@ spec: 8 }} serviceAccountName: {{ include "mondoo-operator.fullname" . }}-controller-manager terminationGracePeriodSeconds: 10 + {{- if .Values.operator.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.operator.imagePullSecrets | nindent 6 }} + {{- end }} diff --git a/charts/mondoo-operator/templates/mondooauditconfig-crd.yaml b/charts/mondoo-operator/templates/mondooauditconfig-crd.yaml deleted file mode 100644 index a185dcb80..000000000 --- a/charts/mondoo-operator/templates/mondooauditconfig-crd.yaml +++ /dev/null @@ -1,1249 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: mondooauditconfigs.k8s.mondoo.com - labels: - {{- include "mondoo-operator.labels" . | nindent 4 }} -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - name: webhook-service - namespace: '{{ .Release.Namespace }}' - path: /convert - conversionReviewVersions: - - v1 - group: k8s.mondoo.com - names: - kind: MondooAuditConfig - listKind: MondooAuditConfigList - plural: mondooauditconfigs - singular: mondooauditconfig - scope: Namespaced - versions: - - name: v1alpha2 - schema: - openAPIV3Schema: - description: MondooAuditConfig is the Schema for the mondooauditconfigs API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: MondooAuditConfigSpec defines the desired state of MondooAuditConfig - properties: - admission: - description: |- - Admission is DEPRECATED and ignored. Admission webhooks were removed in v12.1.0. - The operator will automatically clean up any orphaned admission resources. - See docs/admission-migration-guide.md for migration instructions. - properties: - certificateProvisioning: - description: CertificateProvisioning is DEPRECATED. - properties: - mode: - description: Mode is DEPRECATED. - type: string - type: object - enable: - description: Enable is DEPRECATED. Admission webhooks are no longer supported. - type: boolean - image: - description: Image is DEPRECATED. - properties: - digest: - description: |- - Digest specifies the image digest (e.g., sha256:abc123...). - When specified, this takes precedence over Tag. - type: string - name: - type: string - tag: - type: string - type: object - mode: - description: Mode is DEPRECATED. - type: string - replicas: - description: Replicas is DEPRECATED. - format: int32 - type: integer - serviceAccountName: - description: ServiceAccountName is DEPRECATED. - type: string - type: object - annotations: - additionalProperties: - type: string - description: |- - Annotations allows adding custom annotations to all scanned assets. These key-value pairs - will be attached to every asset discovered by the operator, making them searchable - and filterable in the Mondoo Console. - type: object - consoleIntegration: - properties: - enable: - type: boolean - type: object - containers: - properties: - enable: - type: boolean - env: - description: |- - Env allows setting extra environment variables for the node scanner. If the operator sets already an env - variable with the same name, the value specified here will override it. - items: - description: EnvVar represents an environment variable present in a Container. - properties: - name: - description: |- - Name of the environment variable. - May consist of any printable ASCII characters except '='. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - fileKeyRef: - description: |- - FileKeyRef selects a key of the env file. - Requires the EnvFiles feature gate to be enabled. - properties: - key: - description: |- - The key within the env file. An invalid key will prevent the pod from starting. - The keys defined within a source may consist of any printable ASCII characters except '='. - During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. - type: string - optional: - default: false - description: |- - Specify whether the file or its key must be defined. If the file or key - does not exist, then the env var is not published. - If optional is set to true and the specified key does not exist, - the environment variable will not be set in the Pod's containers. - - If optional is set to false and the specified key does not exist, - an error will be returned during Pod creation. - type: boolean - path: - description: |- - The path within the volume from which to select the file. - Must be relative and may not contain the '..' path or start with '..'. - type: string - volumeName: - description: The name of the volume mount containing the env file. - type: string - required: - - key - - path - - volumeName - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's namespace - properties: - key: - description: The key of the secret to select from. Must be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - resources: - description: ResourceRequirements describes the compute resource requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This field depends on the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - type: object - schedule: - description: Specify a custom crontab schedule for the container image scanning job. If not specified, the default schedule is used. - type: string - type: object - filtering: - properties: - namespaces: - properties: - exclude: - description: |- - Exclude is the list of resources to ignore for any watching/scanning actions. Use this if - the goal is to watch/scan all resources except for this Exclude list. - items: - type: string - type: array - include: - description: |- - Include is the list of resources to watch/scan. Setting Include overrides anything in the - Exclude list as specifying an Include list is effectively excluding everything except for what - is on the Include list. - items: - type: string - type: array - type: object - type: object - kubernetesResources: - properties: - containerImageScanning: - description: |- - DEPRECATED: ContainerImageScanning determines whether container images are being scanned. The current implementation - runs a separate job once every 24h that scans the container images running in the cluster. - type: boolean - enable: - type: boolean - externalClusters: - description: |- - ExternalClusters defines remote K8s clusters to scan from this operator instance. - Each external cluster will have its own CronJob created with the appropriate kubeconfig. - items: - description: ExternalCluster defines configuration for scanning a remote K8s cluster - properties: - containerImageScanning: - description: ContainerImageScanning enables scanning of container images in this external cluster. - type: boolean - filtering: - description: |- - Filtering allows namespace filtering specific to this external cluster. - If not specified, uses the global filtering from MondooAuditConfigSpec.Filtering. - properties: - namespaces: - properties: - exclude: - description: |- - Exclude is the list of resources to ignore for any watching/scanning actions. Use this if - the goal is to watch/scan all resources except for this Exclude list. - items: - type: string - type: array - include: - description: |- - Include is the list of resources to watch/scan. Setting Include overrides anything in the - Exclude list as specifying an Include list is effectively excluding everything except for what - is on the Include list. - items: - type: string - type: array - type: object - type: object - kubeconfigSecretRef: - description: |- - KubeconfigSecretRef references a Secret containing kubeconfig for the remote cluster. - The Secret must have a key "kubeconfig" with the kubeconfig content. - Mutually exclusive with ServiceAccountAuth and WorkloadIdentity. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - name: - description: |- - Name is a unique identifier for this cluster (used in resource names). - Must be a valid Kubernetes name (lowercase, alphanumeric, hyphens allowed). - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - privateRegistriesPullSecretRef: - description: |- - PrivateRegistriesPullSecretRef references a Secret containing registry credentials - for pulling/scanning private images in this remote cluster. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - schedule: - description: |- - Schedule overrides the default schedule for this cluster (optional). - If not specified, uses the schedule from KubernetesResources.Schedule. - type: string - serviceAccountAuth: - description: |- - ServiceAccountAuth configures authentication using a service account token. - Mutually exclusive with KubeconfigSecretRef and WorkloadIdentity. - properties: - credentialsSecretRef: - description: |- - CredentialsSecretRef references a Secret containing: - - "token": The service account token (required) - - "ca.crt": The CA certificate (required) - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - server: - description: |- - Server is the URL of the Kubernetes API server. - Example: "https://my-cluster.example.com:6443" - pattern: ^https://.* - type: string - skipTLSVerify: - default: false - description: SkipTLSVerify skips TLS verification. NOT RECOMMENDED for production. - type: boolean - required: - - credentialsSecretRef - - server - type: object - spiffeAuth: - description: |- - SPIFFEAuth configures SPIFFE/SPIRE-based authentication using X.509 SVIDs. - Mutually exclusive with KubeconfigSecretRef, ServiceAccountAuth, and WorkloadIdentity. - properties: - audience: - description: |- - Audience is the intended audience for the SVID (optional). - Some SPIRE configurations require this for workload attestation. - type: string - server: - description: |- - Server is the URL of the remote Kubernetes API server. - Example: "https://remote-cluster.example.com:6443" - pattern: ^https://.* - type: string - socketPath: - default: /run/spire/sockets/agent.sock - description: |- - SocketPath is the path to the SPIRE agent's Workload API socket. - Defaults to "/run/spire/sockets/agent.sock" if not specified. - type: string - trustBundleSecretRef: - description: |- - TrustBundleSecretRef references a Secret containing the remote cluster's - CA certificate for TLS verification. The Secret must have a key "ca.crt". - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - required: - - server - - trustBundleSecretRef - type: object - workloadIdentity: - description: |- - WorkloadIdentity configures cloud-native Workload Identity Federation authentication. - Mutually exclusive with KubeconfigSecretRef, ServiceAccountAuth, and SPIFFEAuth. - properties: - aks: - description: |- - AKS contains AKS-specific Azure Workload Identity configuration. - Required when provider is "aks". - properties: - clientId: - description: ClientID is the Azure AD app client ID. - type: string - clusterName: - description: ClusterName is the AKS cluster name. - type: string - resourceGroup: - description: ResourceGroup containing the AKS cluster. - type: string - subscriptionId: - description: SubscriptionID is the Azure subscription. - type: string - tenantId: - description: TenantID is the Azure AD tenant ID. - type: string - required: - - clientId - - clusterName - - resourceGroup - - subscriptionId - - tenantId - type: object - eks: - description: |- - EKS contains EKS-specific IRSA configuration. - Required when provider is "eks". - properties: - clusterName: - description: ClusterName is the EKS cluster name. - type: string - region: - description: Region is the AWS region. - type: string - roleArn: - description: |- - RoleARN is the IAM role to assume. - Format: arn:aws:iam:::role/ - type: string - required: - - clusterName - - region - - roleArn - type: object - gke: - description: |- - GKE contains GKE-specific Workload Identity configuration. - Required when provider is "gke". - properties: - clusterLocation: - description: ClusterLocation is the region or zone (e.g., "us-central1" or "us-central1-a"). - type: string - clusterName: - description: ClusterName is the GKE cluster name. - type: string - googleServiceAccount: - description: |- - GoogleServiceAccount is the Google service account to impersonate. - Format: @.iam.gserviceaccount.com - type: string - projectId: - description: ProjectID is the GCP project ID. - type: string - required: - - clusterLocation - - clusterName - - googleServiceAccount - - projectId - type: object - provider: - description: Provider specifies the cloud provider for WIF. - enum: - - gke - - eks - - aks - type: string - required: - - provider - type: object - required: - - name - type: object - type: array - resourceWatcher: - description: |- - ResourceWatcher configures real-time resource watching and scanning. - When enabled, a deployment will be created that watches for K8s resource changes - and scans them immediately rather than waiting for the CronJob schedule. - properties: - debounceInterval: - default: 10s - description: |- - DebounceInterval specifies how long to batch changes before triggering a scan. - This prevents excessive scanning when multiple resources change in quick succession. - Default is 10 seconds. - type: string - enable: - description: |- - Enable enables real-time resource watching and scanning. - When enabled, a deployment will be created that watches K8s resources for changes - and scans them using cnspec. - type: boolean - minimumScanInterval: - default: 2m - description: |- - MinimumScanInterval specifies the minimum time between scans (rate limit). - This provides a hard limit on scan frequency even when resources are changing continuously. - Default is 2 minutes. - type: string - resourceTypes: - description: |- - ResourceTypes specifies which resource types to watch. If not specified, defaults are used - based on WatchAllResources setting. When WatchAllResources is false (default), defaults to: - deployments, daemonsets, statefulsets, replicasets. When true, defaults to: - pods, deployments, daemonsets, statefulsets, replicasets, jobs, cronjobs, services, ingresses, namespaces - items: - type: string - type: array - watchAllResources: - description: |- - WatchAllResources controls whether to watch all resource types or only high-priority ones. - When false (default), only watches stable workload resources: Deployments, DaemonSets, - StatefulSets, and ReplicaSets. When true, watches all resources including ephemeral ones - like Pods, Jobs, and CronJobs. - type: boolean - type: object - schedule: - description: Specify a custom crontab schedule for the Kubernetes resource scanning job. If not specified, the default schedule is used. - type: string - type: object - mondooCredsSecretRef: - description: Config is an example field of MondooAuditConfig. Edit mondooauditconfig_types.go to remove/update - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - mondooTokenSecretRef: - description: |- - MondooTokenSecretRef can optionally hold a time-limited token that the mondoo-operator will use - to create a Mondoo service account saved to the Secret specified in .spec.mondooCredsSecretRef - if that Secret does not exist. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - nodes: - properties: - enable: - type: boolean - env: - description: |- - Env allows setting extra environment variables for the node scanner. If the operator sets already an env - variable with the same name, the value specified here will override it. - items: - description: EnvVar represents an environment variable present in a Container. - properties: - name: - description: |- - Name of the environment variable. - May consist of any printable ASCII characters except '='. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - fileKeyRef: - description: |- - FileKeyRef selects a key of the env file. - Requires the EnvFiles feature gate to be enabled. - properties: - key: - description: |- - The key within the env file. An invalid key will prevent the pod from starting. - The keys defined within a source may consist of any printable ASCII characters except '='. - During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. - type: string - optional: - default: false - description: |- - Specify whether the file or its key must be defined. If the file or key - does not exist, then the env var is not published. - If optional is set to true and the specified key does not exist, - the environment variable will not be set in the Pod's containers. - - If optional is set to false and the specified key does not exist, - an error will be returned during Pod creation. - type: boolean - path: - description: |- - The path within the volume from which to select the file. - Must be relative and may not contain the '..' path or start with '..'. - type: string - volumeName: - description: The name of the volume mount containing the env file. - type: string - required: - - key - - path - - volumeName - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's namespace - properties: - key: - description: The key of the secret to select from. Must be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - intervalTimer: - default: 60 - description: |- - IntervalTimer is the interval (in minutes) for the node scanning. The default is "60". Only applicable for Deployment - style. - type: integer - priorityClassName: - description: PriorityClassName specifies the name of the PriorityClass for the node scanning workloads. - type: string - resources: - description: ResourceRequirements describes the compute resource requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This field depends on the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - type: object - schedule: - description: |- - Schedule specifies a custom crontab schedule for the node scanning job. If not specified, the default schedule is - used. Only applicable for CronJob style - type: string - style: - default: cronjob - description: Style specifies how node scanning is deployed. The default is "cronjob" which will create a CronJob for the node scanning. - enum: - - cronjob - - deployment - - daemonset - type: string - type: object - scanner: - description: |- - Scanner defines the settings for the Mondoo scanner that will be running in the cluster. The same scanner - is used for scanning the Kubernetes API and the nodes. - properties: - env: - description: |- - Env allows setting extra environment variables for the scanner. If the operator sets already an env - variable with the same name, the value specified here will override it. - items: - description: EnvVar represents an environment variable present in a Container. - properties: - name: - description: |- - Name of the environment variable. - May consist of any printable ASCII characters except '='. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - fileKeyRef: - description: |- - FileKeyRef selects a key of the env file. - Requires the EnvFiles feature gate to be enabled. - properties: - key: - description: |- - The key within the env file. An invalid key will prevent the pod from starting. - The keys defined within a source may consist of any printable ASCII characters except '='. - During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. - type: string - optional: - default: false - description: |- - Specify whether the file or its key must be defined. If the file or key - does not exist, then the env var is not published. - If optional is set to true and the specified key does not exist, - the environment variable will not be set in the Pod's containers. - - If optional is set to false and the specified key does not exist, - an error will be returned during Pod creation. - type: boolean - path: - description: |- - The path within the volume from which to select the file. - Must be relative and may not contain the '..' path or start with '..'. - type: string - volumeName: - description: The name of the volume mount containing the env file. - type: string - required: - - key - - path - - volumeName - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's namespace - properties: - key: - description: The key of the secret to select from. Must be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - image: - properties: - digest: - description: |- - Digest specifies the image digest (e.g., sha256:abc123...). - When specified, this takes precedence over Tag. - type: string - name: - type: string - tag: - type: string - type: object - privateRegistriesPullSecretRef: - description: |- - PrivateRegistriesPullSecretRef defines the name of a secret that contains the credentials for the private - registries we have to pull images from. Use this when you have a single secret containing credentials - for one or more registries. For multiple separate secrets, use PrivateRegistriesPullSecretRefs instead. - Deprecated: Use PrivateRegistriesPullSecretRefs for new configurations. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - privateRegistriesPullSecretRefs: - description: |- - PrivateRegistriesPullSecretRefs defines a list of secrets that contain credentials for private - registries. Use this when you need to pull images from multiple private registries and the credentials - are stored in separate secrets (e.g., managed by different teams or external secret operators). - The credentials from all secrets will be merged. If both PrivateRegistriesPullSecretRef and - PrivateRegistriesPullSecretRefs are specified, all secrets will be merged together. - items: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - type: array - replicas: - default: 1 - description: |- - Number of replicas for the scanner. - For enforcing mode, the minimum should be two to prevent problems during Pod failures, - e.g. node failure, node scaling, etc. - format: int32 - minimum: 1 - type: integer - resources: - description: ResourceRequirements describes the compute resource requirements. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This field depends on the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - type: object - serviceAccountName: - default: mondoo-operator-k8s-resources-scanning - type: string - type: object - required: - - mondooCredsSecretRef - type: object - status: - description: MondooAuditConfigStatus defines the observed state of MondooAuditConfig - properties: - conditions: - description: Conditions includes detailed status for the MondooAuditConfig - items: - properties: - affectedPods: - description: AffectedPods, when filled, contains a list which are affected by an issue - items: - type: string - type: array - lastTransitionTime: - description: LastTransitionTime is the last time the condition transitioned from one status to another. - format: date-time - type: string - lastUpdateTime: - description: LastUpdateTime is the last time we probed the condition - format: date-time - type: string - memoryLimit: - description: MemoryLimit contains the currently active memory limit for a Pod - type: string - message: - description: Message is a human-readable message indicating details about the last transition - type: string - reason: - description: Reason is a unique, one-word, CamelCase reason for the condition's last transition - type: string - status: - description: Status is the status of the condition - type: string - type: - description: Type is the specific type of the condition - type: string - required: - - status - - type - type: object - type: array - pods: - description: Pods store the name of the pods which are running mondoo instances - items: - type: string - type: array - reconciledByOperatorVersion: - description: ReconciledByOperatorVersion contains the version of the operator which reconciled this MondooAuditConfig - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/charts/mondoo-operator/templates/mondoooperatorconfig-crd.yaml b/charts/mondoo-operator/templates/mondoooperatorconfig-crd.yaml deleted file mode 100644 index 493558df7..000000000 --- a/charts/mondoo-operator/templates/mondoooperatorconfig-crd.yaml +++ /dev/null @@ -1,104 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: mondoooperatorconfigs.k8s.mondoo.com - labels: - {{- include "mondoo-operator.labels" . | nindent 4 }} -spec: - group: k8s.mondoo.com - names: - kind: MondooOperatorConfig - listKind: MondooOperatorConfigList - plural: mondoooperatorconfigs - singular: mondoooperatorconfig - scope: Cluster - versions: - - name: v1alpha2 - schema: - openAPIV3Schema: - description: MondooOperatorConfig is the Schema for the mondoooperatorconfigs API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: MondooOperatorConfigSpec defines the desired state of MondooOperatorConfig - properties: - containerProxy: - description: ContainerProxy specifies a proxy to use for container images. - type: string - httpProxy: - description: HttpProxy specifies a proxy to use for HTTP requests to the Mondoo Platform. - type: string - metrics: - description: Metrics controls the enabling/disabling of metrics report of mondoo-operator - properties: - enable: - type: boolean - resourceLabels: - additionalProperties: - type: string - description: |- - ResourceLabels allows providing a list of extra labels to apply to the metrics-related - resources (eg. ServiceMonitor) - type: object - type: object - skipContainerResolution: - description: Allows skipping Image resolution from upstream repository - type: boolean - type: object - status: - description: MondooOperatorConfigStatus defines the observed state of MondooOperatorConfig - properties: - conditions: - description: Conditions includes more detailed status for the mondoo config - items: - description: Condition contains details for the current condition of a MondooOperatorConfig - properties: - lastTransitionTime: - description: LastTransitionTime is the last time the condition transitioned from one status to another. - format: date-time - type: string - lastUpdateTime: - description: LastUpdateTime is the last time the condition was updated. - format: date-time - type: string - message: - description: Message is a human-readable message indicating details about last transition. - type: string - reason: - description: Reason is a unique, one-word, CamelCase reason for the condition's last transition. - type: string - status: - description: Status is the status of the condition. - type: string - type: - description: Type is the type of the condition. - type: string - required: - - status - - type - type: object - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/charts/mondoo-operator/templates/mondoooperatorconfig.yaml b/charts/mondoo-operator/templates/mondoooperatorconfig.yaml new file mode 100644 index 000000000..e794ee3b6 --- /dev/null +++ b/charts/mondoo-operator/templates/mondoooperatorconfig.yaml @@ -0,0 +1,38 @@ +{{- if .Values.operator.createConfig }} +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config + labels: + {{- include "mondoo-operator.labels" . | nindent 4 }} +spec: + {{- if .Values.operator.httpProxy }} + httpProxy: {{ .Values.operator.httpProxy | quote }} + {{- end }} + {{- if .Values.operator.httpsProxy }} + httpsProxy: {{ .Values.operator.httpsProxy | quote }} + {{- end }} + {{- if .Values.operator.noProxy }} + noProxy: {{ .Values.operator.noProxy | quote }} + {{- end }} + {{- if .Values.operator.containerProxy }} + containerProxy: {{ .Values.operator.containerProxy | quote }} + {{- end }} + {{- if .Values.operator.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.operator.imagePullSecrets | nindent 4 }} + {{- end }} + {{- if .Values.operator.imageRegistry }} + imageRegistry: {{ .Values.operator.imageRegistry | quote }} + {{- end }} + {{- if .Values.operator.registryMirrors }} + registryMirrors: + {{- toYaml .Values.operator.registryMirrors | nindent 4 }} + {{- end }} + {{- if .Values.operator.skipContainerResolution }} + skipContainerResolution: {{ .Values.operator.skipContainerResolution }} + {{- end }} + {{- if .Values.operator.skipProxyForCnspec }} + skipProxyForCnspec: {{ .Values.operator.skipProxyForCnspec }} + {{- end }} +{{- end }} diff --git a/charts/mondoo-operator/values.yaml b/charts/mondoo-operator/values.yaml index 437c17f4a..2e480ebae 100644 --- a/charts/mondoo-operator/values.yaml +++ b/charts/mondoo-operator/values.yaml @@ -60,3 +60,43 @@ cleanup: enabled: true # Timeout for waiting for MondooAuditConfig resources to be deleted timeout: 2m + +# Mondoo Operator Configuration +# These settings are applied to the MondooOperatorConfig custom resource +operator: + # Create MondooOperatorConfig resource (set to false on first install, then true on upgrade) + createConfig: false + # HTTP proxy for outbound connections to Mondoo Platform + httpProxy: "" + # HTTPS proxy for outbound connections to Mondoo Platform + httpsProxy: "" + # Comma-separated list of hosts that should bypass the proxy + noProxy: "" + # Container proxy for pulling container images (used in container image scanning) + containerProxy: "" + # Image pull secrets for pulling Mondoo images from private registries + # Example: + # imagePullSecrets: + # - name: my-registry-secret + imagePullSecrets: [] + # Custom image registry prefix for corporate registries (deprecated, use registryMirrors) + # Example: "registry.example.com/ghcr.io.docker" + # This will rewrite image references like: + # ghcr.io/mondoohq/mondoo-operator:v1.0.0 -> registry.example.com/ghcr.io.docker/mondoohq/mondoo-operator:v1.0.0 + imageRegistry: "" + # Registry mirrors for mapping public registries to private mirrors + # This is more flexible than imageRegistry as it supports multiple registries + # Example: + # registryMirrors: + # ghcr.io: registry.example.com/ghcr.io.docker + # docker.io: registry.example.com/hub.docker.com + # quay.io: registry.example.com/quay.io + registryMirrors: {} + + # Skip proxy settings for cnspec-based components (scan-api, container scanning) + # Set to true when using an internal Mondoo API mirror that doesn't require proxy + # but other components still need proxy for external access (e.g., image resolution) + # Default: false (proxy settings are applied to all components) + skipProxyForCnspec: false + # Skip container image resolution (useful for air-gapped environments) + skipContainerResolution: false diff --git a/config/crd/bases/k8s.mondoo.com_mondoooperatorconfigs.yaml b/config/crd/bases/k8s.mondoo.com_mondoooperatorconfigs.yaml index 2958ed4b1..3121a0c37 100644 --- a/config/crd/bases/k8s.mondoo.com_mondoooperatorconfigs.yaml +++ b/config/crd/bases/k8s.mondoo.com_mondoooperatorconfigs.yaml @@ -48,6 +48,38 @@ spec: description: HttpProxy specifies a proxy to use for HTTP requests to the Mondoo Platform. type: string + httpsProxy: + description: HttpsProxy specifies a proxy to use for HTTPS requests + to the Mondoo Platform. + type: string + imagePullSecrets: + description: |- + ImagePullSecrets specifies the name of the Secret to use for pulling images for all Mondoo components. + The secret must be of type kubernetes.io/dockerconfigjson. + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + imageRegistry: + description: |- + ImageRegistry specifies a custom container image registry to use for all Mondoo images. + This allows using a private registry mirror (e.g., artifactory.example.com/ghcr.io.docker). + If set, all image references will be prefixed with this registry. + Deprecated: Use RegistryMirrors for more flexible registry mapping. + type: string metrics: description: Metrics controls the enabling/disabling of metrics report of mondoo-operator @@ -62,9 +94,32 @@ spec: resources (eg. ServiceMonitor) type: object type: object + noProxy: + description: NoProxy specifies a comma-separated list of hosts that + should not use the proxy. + type: string + registryMirrors: + additionalProperties: + type: string + description: |- + RegistryMirrors specifies a mapping of public registries to private mirrors. + The key is the public registry (e.g., "ghcr.io", "docker.io", "quay.io") + and the value is the private mirror (e.g., "artifactory.example.com/ghcr.io.docker"). + Example: + registryMirrors: + ghcr.io: artifactory.example.com/ghcr.io.docker + docker.io: artifactory.example.com/hub.docker.com + type: object skipContainerResolution: description: Allows skipping Image resolution from upstream repository type: boolean + skipProxyForCnspec: + description: |- + SkipProxyForCnspec disables proxy environment variables for cnspec-based components + (scan-api, container scanning). Use this when the Mondoo API is accessible directly + without proxy (e.g., internal mirror) but other components need proxy for external access. + Default: false (proxy settings are applied to all components) + type: boolean type: object status: description: MondooOperatorConfigStatus defines the observed state of diff --git a/controllers/container_image/resources.go b/controllers/container_image/resources.go index 5080b184a..a7fb7a1d2 100644 --- a/controllers/container_image/resources.go +++ b/controllers/container_image/resources.go @@ -38,12 +38,31 @@ func CronJob(image, integrationMrn, clusterUid, privateRegistrySecretName string "--inventory-file", "/etc/opt/mondoo/config/inventory.yml", } - if cfg.Spec.HttpProxy != nil { + // Only add proxy settings if SkipProxyForCnspec is false + // cnspec-based components may not properly handle NO_PROXY for internal domains + if !cfg.Spec.SkipProxyForCnspec && cfg.Spec.HttpProxy != nil { cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) } envVars := feature_flags.AllFeatureFlagsAsEnv() envVars = append(envVars, corev1.EnvVar{Name: "MONDOO_AUTO_UPDATE", Value: "false"}) + + // Add proxy environment variables from MondooOperatorConfig only if SkipProxyForCnspec is false + if !cfg.Spec.SkipProxyForCnspec { + if cfg.Spec.HttpProxy != nil { + envVars = append(envVars, corev1.EnvVar{Name: "HTTP_PROXY", Value: *cfg.Spec.HttpProxy}) + envVars = append(envVars, corev1.EnvVar{Name: "http_proxy", Value: *cfg.Spec.HttpProxy}) + } + if cfg.Spec.HttpsProxy != nil { + envVars = append(envVars, corev1.EnvVar{Name: "HTTPS_PROXY", Value: *cfg.Spec.HttpsProxy}) + envVars = append(envVars, corev1.EnvVar{Name: "https_proxy", Value: *cfg.Spec.HttpsProxy}) + } + if cfg.Spec.NoProxy != nil { + envVars = append(envVars, corev1.EnvVar{Name: "NO_PROXY", Value: *cfg.Spec.NoProxy}) + envVars = append(envVars, corev1.EnvVar{Name: "no_proxy", Value: *cfg.Spec.NoProxy}) + } + } + envVars = k8s.MergeEnv(envVars, m.Spec.Containers.Env) cronjob := &batchv1.CronJob{ @@ -152,6 +171,11 @@ func CronJob(image, integrationMrn, clusterUid, privateRegistrySecretName string // Add private registry secret if specified k8s.AddPrivateRegistryPullSecretToSpec(&cronjob.Spec.JobTemplate.Spec.Template.Spec, privateRegistrySecretName) + // Add imagePullSecrets from MondooOperatorConfig + if len(cfg.Spec.ImagePullSecrets) > 0 { + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = cfg.Spec.ImagePullSecrets + } + return cronjob } diff --git a/controllers/integration/integration_controller.go b/controllers/integration/integration_controller.go index ada9cb74f..1bf4e7289 100644 --- a/controllers/integration/integration_controller.go +++ b/controllers/integration/integration_controller.go @@ -123,7 +123,7 @@ func (r *IntegrationReconciler) processMondooAuditConfig(m v1alpha2.MondooAuditC } } - if err = mondoo.IntegrationCheckIn(r.ctx, integrationMrn, *serviceAccount, r.MondooClientBuilder, config.Spec.HttpProxy, logger); err != nil { + if err = mondoo.IntegrationCheckIn(r.ctx, integrationMrn, *serviceAccount, r.MondooClientBuilder, config.Spec.HttpProxy, config.Spec.NoProxy, logger); err != nil { logger.Error(err, "failed to CheckIn() for integration", "integrationMRN", string(integrationMrn)) return err } diff --git a/controllers/k8s_scan/resources.go b/controllers/k8s_scan/resources.go index 1578e7b01..ddb7cda66 100644 --- a/controllers/k8s_scan/resources.go +++ b/controllers/k8s_scan/resources.go @@ -59,11 +59,12 @@ func CronJob(image string, m *v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOpe "--score-threshold", "0", } - if cfg.Spec.HttpProxy != nil { + // Only add proxy if configured and not skipped for cnspec + if cfg.Spec.HttpProxy != nil && !cfg.Spec.SkipProxyForCnspec { cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) } - envVars := feature_flags.AllFeatureFlagsAsEnv() + envVars := buildEnvVars(cfg) envVars = append(envVars, corev1.EnvVar{Name: "MONDOO_AUTO_UPDATE", Value: "false"}) cronjob := &batchv1.CronJob{ @@ -160,6 +161,7 @@ func CronJob(image string, m *v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOpe }, }, }, + ImagePullSecrets: cfg.Spec.ImagePullSecrets, }, }, }, @@ -183,11 +185,12 @@ func ExternalClusterCronJob(image string, cluster v1alpha2.ExternalCluster, m *v "--score-threshold", "0", } - if cfg.Spec.HttpProxy != nil { + // Only add proxy if configured and not skipped for cnspec + if cfg.Spec.HttpProxy != nil && !cfg.Spec.SkipProxyForCnspec { cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) } - envVars := feature_flags.AllFeatureFlagsAsEnv() + envVars := buildEnvVars(cfg) envVars = append(envVars, corev1.EnvVar{Name: "MONDOO_AUTO_UPDATE", Value: "false"}) // Point KUBECONFIG to the mounted kubeconfig file envVars = append(envVars, corev1.EnvVar{Name: "KUBECONFIG", Value: "/etc/opt/mondoo/kubeconfig/kubeconfig"}) @@ -1021,3 +1024,23 @@ func ExternalClusterInventory(integrationMRN, operatorClusterUID string, cluster return string(invBytes), nil } + +func buildEnvVars(cfg v1alpha2.MondooOperatorConfig) []corev1.EnvVar { + envVars := feature_flags.AllFeatureFlagsAsEnv() + + // Add proxy environment variables from MondooOperatorConfig + if cfg.Spec.HttpProxy != nil { + envVars = append(envVars, corev1.EnvVar{Name: "HTTP_PROXY", Value: *cfg.Spec.HttpProxy}) + envVars = append(envVars, corev1.EnvVar{Name: "http_proxy", Value: *cfg.Spec.HttpProxy}) + } + if cfg.Spec.HttpsProxy != nil { + envVars = append(envVars, corev1.EnvVar{Name: "HTTPS_PROXY", Value: *cfg.Spec.HttpsProxy}) + envVars = append(envVars, corev1.EnvVar{Name: "https_proxy", Value: *cfg.Spec.HttpsProxy}) + } + if cfg.Spec.NoProxy != nil { + envVars = append(envVars, corev1.EnvVar{Name: "NO_PROXY", Value: *cfg.Spec.NoProxy}) + envVars = append(envVars, corev1.EnvVar{Name: "no_proxy", Value: *cfg.Spec.NoProxy}) + } + + return envVars +} diff --git a/controllers/mondooauditconfig_controller.go b/controllers/mondooauditconfig_controller.go index 5406773f9..c716ba401 100644 --- a/controllers/mondooauditconfig_controller.go +++ b/controllers/mondooauditconfig_controller.go @@ -63,7 +63,7 @@ var MondooClientBuilder = mondooclient.NewClient //+kubebuilder:rbac:groups=apps,resources=deployments;daemonsets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=apps,resources=deployments;replicasets;daemonsets;statefulsets,verbs=get;list;watch //+kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=deletecollection +//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=delete;deletecollection //+kubebuilder:rbac:groups=batch,resources=cronjobs;jobs,verbs=get;list;watch //+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core,resources=pods;namespaces;nodes,verbs=get;list;watch @@ -131,6 +131,18 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re config = &v1alpha2.MondooOperatorConfig{} } + // Apply registry configuration to the container image resolver + imageResolver := r.ContainerImageResolver + if len(config.Spec.RegistryMirrors) > 0 { + imageResolver = imageResolver.WithRegistryMirrors(config.Spec.RegistryMirrors) + } else if config.Spec.ImageRegistry != nil && *config.Spec.ImageRegistry != "" { + imageResolver = imageResolver.WithImageRegistry(*config.Spec.ImageRegistry) + } + // Apply imagePullSecrets for authentication when resolving images + if len(config.Spec.ImagePullSecrets) > 0 { + imageResolver = imageResolver.WithImagePullSecrets(config.Spec.ImagePullSecrets) + } + if !mondooAuditConfig.DeletionTimestamp.IsZero() { log.Info("deleting") @@ -262,7 +274,7 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re Mondoo: mondooAuditConfig, KubeClient: r.Client, MondooOperatorConfig: config, - ContainerImageResolver: r.ContainerImageResolver, + ContainerImageResolver: imageResolver, IsOpenshift: r.RunningOnOpenShift, } @@ -277,7 +289,7 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re containers := container_image.DeploymentHandler{ Mondoo: mondooAuditConfig, KubeClient: r.Client, - ContainerImageResolver: r.ContainerImageResolver, + ContainerImageResolver: imageResolver, MondooOperatorConfig: config, } @@ -293,7 +305,7 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re Mondoo: mondooAuditConfig, KubeClient: r.Client, MondooOperatorConfig: config, - ContainerImageResolver: r.ContainerImageResolver, + ContainerImageResolver: imageResolver, MondooClientBuilder: r.MondooClientBuilder, } @@ -309,7 +321,7 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re Mondoo: mondooAuditConfig, KubeClient: r.Client, MondooOperatorConfig: config, - ContainerImageResolver: r.ContainerImageResolver, + ContainerImageResolver: imageResolver, } result, reconcileError = resourceWatcher.Reconcile(ctx) @@ -490,6 +502,7 @@ func (r *MondooAuditConfigReconciler) exchangeTokenForServiceAccount(ctx context client.ObjectKeyFromObject(mondooCredsSecret), tokenData, cfg.Spec.HttpProxy, + cfg.Spec.NoProxy, log) } diff --git a/controllers/nodes/resources.go b/controllers/nodes/resources.go index dfea08149..3524cad74 100644 --- a/controllers/nodes/resources.go +++ b/controllers/nodes/resources.go @@ -37,6 +37,24 @@ const ( ignoreAnnotationValue = "ignore" ) +// buildProxyEnvVars builds proxy environment variables from MondooOperatorConfig +func buildProxyEnvVars(cfg v1alpha2.MondooOperatorConfig) []corev1.EnvVar { + var proxyEnvVars []corev1.EnvVar + if cfg.Spec.HttpProxy != nil { + proxyEnvVars = append(proxyEnvVars, corev1.EnvVar{Name: "HTTP_PROXY", Value: *cfg.Spec.HttpProxy}) + proxyEnvVars = append(proxyEnvVars, corev1.EnvVar{Name: "http_proxy", Value: *cfg.Spec.HttpProxy}) + } + if cfg.Spec.HttpsProxy != nil { + proxyEnvVars = append(proxyEnvVars, corev1.EnvVar{Name: "HTTPS_PROXY", Value: *cfg.Spec.HttpsProxy}) + proxyEnvVars = append(proxyEnvVars, corev1.EnvVar{Name: "https_proxy", Value: *cfg.Spec.HttpsProxy}) + } + if cfg.Spec.NoProxy != nil { + proxyEnvVars = append(proxyEnvVars, corev1.EnvVar{Name: "NO_PROXY", Value: *cfg.Spec.NoProxy}) + proxyEnvVars = append(proxyEnvVars, corev1.EnvVar{Name: "no_proxy", Value: *cfg.Spec.NoProxy}) + } + return proxyEnvVars +} + // CronJob creates a CronJob for node scanning func CronJob(image string, node corev1.Node, m *v1alpha2.MondooAuditConfig, isOpenshift bool, cfg v1alpha2.MondooOperatorConfig) *batchv1.CronJob { ls := NodeScanningLabels(*m) @@ -50,10 +68,12 @@ func CronJob(image string, node corev1.Node, m *v1alpha2.MondooAuditConfig, isOp cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) } + proxyEnvVars := buildProxyEnvVars(cfg) + containerResources := k8s.ResourcesRequirementsWithDefaults(m.Spec.Nodes.Resources, k8s.DefaultNodeScanningResources) gcLimit := gomemlimit.CalculateGoMemLimit(containerResources) - return &batchv1.CronJob{ + cj := &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: CronJobName(m.Name, node.Name), Namespace: m.Namespace, @@ -114,13 +134,13 @@ func CronJob(image string, node corev1.Node, m *v1alpha2.MondooAuditConfig, isOp {Name: "config", ReadOnly: true, MountPath: "/etc/opt/"}, {Name: "temp", MountPath: "/tmp"}, }, - Env: k8s.MergeEnv([]corev1.EnvVar{ + Env: k8s.MergeEnv(append([]corev1.EnvVar{ {Name: "DEBUG", Value: "false"}, {Name: "MONDOO_PROCFS", Value: "on"}, {Name: "MONDOO_AUTO_UPDATE", Value: "false"}, {Name: "NODE_NAME", Value: node.Name}, {Name: "GOMEMLIMIT", Value: gcLimit}, - }, m.Spec.Nodes.Env), + }, proxyEnvVars...), m.Spec.Nodes.Env), TerminationMessagePath: "/dev/termination-log", TerminationMessagePolicy: corev1.TerminationMessageReadFile, ImagePullPolicy: corev1.PullIfNotPresent, @@ -166,6 +186,13 @@ func CronJob(image string, node corev1.Node, m *v1alpha2.MondooAuditConfig, isOp }, }, } + + // Add imagePullSecrets from MondooOperatorConfig + if len(cfg.Spec.ImagePullSecrets) > 0 { + cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = cfg.Spec.ImagePullSecrets + } + + return cj } // DaemonSet creates a DaemonSet for node scanning @@ -181,10 +208,12 @@ func DaemonSet(m v1alpha2.MondooAuditConfig, isOpenshift bool, image string, cfg cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) } + proxyEnvVars := buildProxyEnvVars(cfg) + containerResources := k8s.ResourcesRequirementsWithDefaults(m.Spec.Nodes.Resources, k8s.DefaultNodeScanningResources) gcLimit := gomemlimit.CalculateGoMemLimit(containerResources) - return &appsv1.DaemonSet{ + ds := &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: DaemonSetName(m.Name), Namespace: m.Namespace, @@ -234,13 +263,13 @@ func DaemonSet(m v1alpha2.MondooAuditConfig, isOpenshift bool, image string, cfg {Name: "config", ReadOnly: true, MountPath: "/etc/opt/"}, {Name: "temp", MountPath: "/tmp"}, }, - Env: k8s.MergeEnv([]corev1.EnvVar{ + Env: k8s.MergeEnv(append([]corev1.EnvVar{ {Name: "DEBUG", Value: "false"}, {Name: "MONDOO_PROCFS", Value: "on"}, {Name: "MONDOO_AUTO_UPDATE", Value: "false"}, {Name: "GOMEMLIMIT", Value: gcLimit}, {Name: "NODE_NAME", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "spec.nodeName"}}}, - }, m.Spec.Nodes.Env), + }, proxyEnvVars...), m.Spec.Nodes.Env), }, }, Volumes: []corev1.Volume{ @@ -281,6 +310,13 @@ func DaemonSet(m v1alpha2.MondooAuditConfig, isOpenshift bool, image string, cfg }, }, } + + // Add imagePullSecrets from MondooOperatorConfig + if len(cfg.Spec.ImagePullSecrets) > 0 { + ds.Spec.Template.Spec.ImagePullSecrets = cfg.Spec.ImagePullSecrets + } + + return ds } // ConfigMap creates a ConfigMap for node scanning inventory diff --git a/controllers/status/status_reporter.go b/controllers/status/status_reporter.go index 73fa3d901..6c2da3841 100644 --- a/controllers/status/status_reporter.go +++ b/controllers/status/status_reporter.go @@ -82,6 +82,7 @@ func (r *StatusReporter) Report(ctx context.Context, m v1alpha2.MondooAuditConfi ApiEndpoint: serviceAccount.ApiEndpoint, Token: token, HttpProxy: cfg.Spec.HttpProxy, + NoProxy: cfg.Spec.NoProxy, }) if err != nil { return err diff --git a/pkg/client/common/http.go b/pkg/client/common/http.go index 899a38c10..95dc0c300 100644 --- a/pkg/client/common/http.go +++ b/pkg/client/common/http.go @@ -11,6 +11,7 @@ import ( "net" "net/http" "net/url" + "strings" "time" ) @@ -23,6 +24,10 @@ const ( ) func DefaultHttpClient(httpProxy *string, httpTimeout *time.Duration) (http.Client, error) { + return DefaultHttpClientWithNoProxy(httpProxy, nil, httpTimeout) +} + +func DefaultHttpClientWithNoProxy(httpProxy *string, noProxy *string, httpTimeout *time.Duration) (http.Client, error) { tr := &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ @@ -40,7 +45,13 @@ func DefaultHttpClient(httpProxy *string, httpTimeout *time.Duration) (http.Clie if err != nil { return http.Client{}, err } - tr.Proxy = http.ProxyURL(urlParsed) + // Create a proxy function that respects noProxy settings + tr.Proxy = func(req *http.Request) (*url.URL, error) { + if noProxy != nil && shouldBypassProxy(req.URL.Host, *noProxy) { + return nil, nil // No proxy for this host + } + return urlParsed, nil + } } timeout := defaultHttpTimeout if httpTimeout != nil { @@ -52,6 +63,59 @@ func DefaultHttpClient(httpProxy *string, httpTimeout *time.Duration) (http.Clie }, nil } +// shouldBypassProxy checks if the given host should bypass the proxy based on noProxy settings +func shouldBypassProxy(host string, noProxy string) bool { + if noProxy == "" { + return false + } + + // Remove port from host if present + hostWithoutPort := host + if colonIndex := strings.LastIndex(host, ":"); colonIndex != -1 { + hostWithoutPort = host[:colonIndex] + } + + // Check each entry in the noProxy list + for _, entry := range strings.Split(noProxy, ",") { + entry = strings.TrimSpace(entry) + if entry == "" { + continue + } + + // Handle wildcard "*" - bypass all + if entry == "*" { + return true + } + + // Handle domain suffix matching (e.g., ".example.com" matches "api.mondoo.example.com") + if strings.HasPrefix(entry, ".") { + if strings.HasSuffix(hostWithoutPort, entry) || hostWithoutPort == entry[1:] { + return true + } + continue + } + + // Handle CIDR notation for IP ranges + if strings.Contains(entry, "/") { + _, cidr, err := net.ParseCIDR(entry) + if err == nil { + ip := net.ParseIP(hostWithoutPort) + if ip != nil && cidr.Contains(ip) { + return true + } + } + continue + } + + // Exact match or suffix match + if hostWithoutPort == entry || strings.HasSuffix(hostWithoutPort, "."+entry) { + return true + } + } + + return false +} + func Request(ctx context.Context, client http.Client, url, token string, reqBodyBytes []byte) ([]byte, error) { header := make(http.Header) header.Set("Accept", "application/json") diff --git a/pkg/client/mondooclient/client.go b/pkg/client/mondooclient/client.go index b0a3d709f..1f0f37a38 100644 --- a/pkg/client/mondooclient/client.go +++ b/pkg/client/mondooclient/client.go @@ -26,6 +26,7 @@ type MondooClientOptions struct { ApiEndpoint string Token string HttpProxy *string + NoProxy *string HttpTimeout *time.Duration } @@ -37,7 +38,7 @@ type mondooClient struct { func NewClient(opts MondooClientOptions) (MondooClient, error) { opts.ApiEndpoint = strings.TrimRight(opts.ApiEndpoint, "/") - client, err := common.DefaultHttpClient(opts.HttpProxy, opts.HttpTimeout) + client, err := common.DefaultHttpClientWithNoProxy(opts.HttpProxy, opts.NoProxy, opts.HttpTimeout) if err != nil { return nil, err } diff --git a/pkg/imagecache/imagecache.go b/pkg/imagecache/imagecache.go index 102fc96ad..c62b6a3b7 100644 --- a/pkg/imagecache/imagecache.go +++ b/pkg/imagecache/imagecache.go @@ -4,12 +4,18 @@ package imagecache import ( + "context" + "encoding/base64" + "encoding/json" "sync" "time" + "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "go.mondoo.com/cnquery/v12/providers/os/connection/container/auth" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -18,12 +24,15 @@ const ( type ImageCacher interface { GetImage(string) (string, error) + // WithAuth returns a new ImageCacher that uses the provided authentication keychain + WithAuth(keychain authn.Keychain) ImageCacher } type imageCache struct { images map[string]imageData imagesMutex sync.RWMutex fetchImage func(string) (string, error) + keychain authn.Keychain } type imageData struct { @@ -81,13 +90,20 @@ func (i *imageCache) updateImage(image string) error { return nil } -func queryImageWithSHA(image string) (string, error) { +func queryImageWithSHA(image string, keychain authn.Keychain) (string, error) { ref, err := name.ParseReference(image) if err != nil { return "", err } - kc := auth.ConstructKeychain(ref.Name()) + // Use provided keychain if given, otherwise use cnquery's auth which supports ECR, GCR, etc. + var kc authn.Keychain + if keychain != nil { + kc = keychain + } else { + kc = auth.ConstructKeychain(ref.Name()) + } + desc, err := remote.Get(ref, remote.WithAuthFromKeychain(kc)) if err != nil { return "", err @@ -101,7 +117,113 @@ func queryImageWithSHA(image string) (string, error) { func NewImageCacher() ImageCacher { return &imageCache{ - images: map[string]imageData{}, - fetchImage: queryImageWithSHA, + images: map[string]imageData{}, + fetchImage: func(image string) (string, error) { + return queryImageWithSHA(image, nil) + }, + } +} + +func (i *imageCache) WithAuth(keychain authn.Keychain) ImageCacher { + return &imageCache{ + images: map[string]imageData{}, + fetchImage: func(image string) (string, error) { + return queryImageWithSHA(image, keychain) + }, + keychain: keychain, + } +} + +// DockerConfigJSON represents the structure of a Docker config.json file +type DockerConfigJSON struct { + Auths map[string]DockerConfigEntry `json:"auths"` +} + +// DockerConfigEntry represents a single registry entry in Docker config +type DockerConfigEntry struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Auth string `json:"auth,omitempty"` +} + +// KeychainFromSecrets creates an authn.Keychain from Kubernetes imagePullSecrets +func KeychainFromSecrets(ctx context.Context, kubeClient client.Client, namespace string, secretRefs []corev1.LocalObjectReference) (authn.Keychain, error) { + if len(secretRefs) == 0 { + return authn.DefaultKeychain, nil + } + + var configs []DockerConfigJSON + for _, secretRef := range secretRefs { + secret := &corev1.Secret{} + if err := kubeClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: secretRef.Name}, secret); err != nil { + continue // Skip secrets that don't exist + } + + // Handle both .dockerconfigjson and .dockercfg formats + var configData []byte + if data, ok := secret.Data[".dockerconfigjson"]; ok { + configData = data + } else if data, ok := secret.Data[".dockercfg"]; ok { + configData = data + } else { + continue + } + + var config DockerConfigJSON + if err := json.Unmarshal(configData, &config); err != nil { + continue + } + configs = append(configs, config) + } + + return &multiKeychain{configs: configs}, nil +} + +// multiKeychain implements authn.Keychain for multiple Docker configs +type multiKeychain struct { + configs []DockerConfigJSON +} + +func (k *multiKeychain) Resolve(resource authn.Resource) (authn.Authenticator, error) { + registry := resource.RegistryStr() + + for _, config := range k.configs { + if entry, ok := config.Auths[registry]; ok { + return resolveAuth(entry) + } + // Also try with https:// prefix + if entry, ok := config.Auths["https://"+registry]; ok { + return resolveAuth(entry) + } } + + return authn.Anonymous, nil +} + +func resolveAuth(entry DockerConfigEntry) (authn.Authenticator, error) { + if entry.Auth != "" { + decoded, err := base64.StdEncoding.DecodeString(entry.Auth) + if err != nil { + return authn.Anonymous, nil + } + // Auth is base64 encoded "username:password" + parts := string(decoded) + for i, c := range parts { + if c == ':' { + return authn.FromConfig(authn.AuthConfig{ + Username: parts[:i], + Password: parts[i+1:], + }), nil + } + } + } + + if entry.Username != "" && entry.Password != "" { + return authn.FromConfig(authn.AuthConfig{ + Username: entry.Username, + Password: entry.Password, + }), nil + } + + return authn.Anonymous, nil } diff --git a/pkg/utils/mondoo/container_image_resolver.go b/pkg/utils/mondoo/container_image_resolver.go index e9b996a4c..43d54b003 100644 --- a/pkg/utils/mondoo/container_image_resolver.go +++ b/pkg/utils/mondoo/container_image_resolver.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "os" + "strings" "github.com/go-logr/logr" @@ -43,6 +44,18 @@ type ContainerImageResolver interface { // by a digest. If userImage, userTag, or userDigest are empty strings, default values are used. // When userDigest is specified, it takes precedence over userTag. MondooOperatorImage(ctx context.Context, userImage, userTag, userDigest string, skipImageResolution bool) (string, error) + + // WithImageRegistry returns a new ContainerImageResolver that uses the specified image registry. + // This allows rewriting image references to use a custom registry (e.g., corporate Artifactory mirror). + // Deprecated: Use WithRegistryMirrors for more flexible registry mapping. + WithImageRegistry(imageRegistry string) ContainerImageResolver + + // WithRegistryMirrors returns a new ContainerImageResolver that uses the specified registry mirrors. + // The mirrors map public registries to private mirrors (e.g., "ghcr.io" -> "artifactory.example.com/ghcr.io.docker"). + WithRegistryMirrors(registryMirrors map[string]string) ContainerImageResolver + + // WithImagePullSecrets returns a new ContainerImageResolver that uses the specified imagePullSecrets for authentication. + WithImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) ContainerImageResolver } type containerImageResolver struct { @@ -52,9 +65,16 @@ type containerImageResolver struct { kubeClient client.Client operatorPodName string operatorPodNamespace string + imageRegistry string + registryMirrors map[string]string + imagePullSecrets []corev1.LocalObjectReference } func NewContainerImageResolver(kubeClient client.Client, isOpenShift bool) ContainerImageResolver { + return NewContainerImageResolverWithRegistry(kubeClient, isOpenShift, "") +} + +func NewContainerImageResolverWithRegistry(kubeClient client.Client, isOpenShift bool, imageRegistry string) ContainerImageResolver { podName := os.Getenv(PodNameEnvVar) if podName == "" { podName = "mondoo-operator-controller-manager" @@ -71,6 +91,7 @@ func NewContainerImageResolver(kubeClient client.Client, isOpenShift bool) Conta kubeClient: kubeClient, operatorPodName: podName, operatorPodNamespace: podNamespace, + imageRegistry: imageRegistry, } } @@ -127,11 +148,23 @@ func (c *containerImageResolver) MondooOperatorImage(ctx context.Context, userIm } func (c *containerImageResolver) resolveImage(image string, skipImageResolution bool) (string, error) { + // Apply custom image registry prefix if configured + image = c.applyImageRegistry(image) + if skipImageResolution { return image, nil } - imageWithDigest, err := c.imageCacher.GetImage(image) + // Apply authentication if imagePullSecrets are configured + cacher := c.imageCacher + if len(c.imagePullSecrets) > 0 { + keychain, err := imagecache.KeychainFromSecrets(context.Background(), c.kubeClient, c.operatorPodNamespace, c.imagePullSecrets) + if err == nil { + cacher = cacher.WithAuth(keychain) + } + } + + imageWithDigest, err := cacher.GetImage(image) if err != nil { c.logger.Error(err, "failed to resolve image plus digest") return "", err @@ -140,6 +173,111 @@ func (c *containerImageResolver) resolveImage(image string, skipImageResolution return imageWithDigest, nil } +// applyImageRegistry rewrites the image to use a custom registry if configured. +// It first checks registryMirrors for a specific mapping, then falls back to imageRegistry. +// For example, if registryMirrors has "ghcr.io" -> "artifactory.example.com/ghcr.io.docker" and +// the image is "ghcr.io/mondoohq/mondoo-operator:v1.0.0", it will be rewritten to +// "artifactory.example.com/ghcr.io.docker/mondoohq/mondoo-operator:v1.0.0" +func (c *containerImageResolver) applyImageRegistry(image string) string { + // Parse the image to extract registry, repository, and tag + parts := splitImageParts(image) + + // First, check if we have a specific mirror for this registry + if len(c.registryMirrors) > 0 && parts.registry != "" { + if mirror, ok := c.registryMirrors[parts.registry]; ok { + return fmt.Sprintf("%s/%s", mirror, parts.repositoryWithTag) + } + } + + // Fall back to the legacy imageRegistry if set + if c.imageRegistry == "" { + return image + } + + if parts.registry != "" { + // Replace the registry with the custom one + return fmt.Sprintf("%s/%s", c.imageRegistry, parts.repositoryWithTag) + } + + // No registry in the image, just prepend the custom registry + return fmt.Sprintf("%s/%s", c.imageRegistry, image) +} + +type imageParts struct { + registry string + repositoryWithTag string +} + +func splitImageParts(image string) imageParts { + // Find the first slash + slashIdx := -1 + for i, c := range image { + if c == '/' { + slashIdx = i + break + } + } + + if slashIdx == -1 { + // No slash, no registry (e.g., "ubuntu:latest") + return imageParts{registry: "", repositoryWithTag: image} + } + + potentialRegistry := image[:slashIdx] + // Check if it looks like a registry (contains a dot or colon, or is "localhost") + if strings.Contains(potentialRegistry, ".") || strings.Contains(potentialRegistry, ":") || potentialRegistry == "localhost" { + return imageParts{ + registry: potentialRegistry, + repositoryWithTag: image[slashIdx+1:], + } + } + + // No registry, the first part is part of the repository (e.g., "library/ubuntu:latest") + return imageParts{registry: "", repositoryWithTag: image} +} + +func (c *containerImageResolver) WithImageRegistry(imageRegistry string) ContainerImageResolver { + return &containerImageResolver{ + logger: c.logger, + resolveForOpenShift: c.resolveForOpenShift, + imageCacher: c.imageCacher, + kubeClient: c.kubeClient, + operatorPodName: c.operatorPodName, + operatorPodNamespace: c.operatorPodNamespace, + imageRegistry: imageRegistry, + registryMirrors: c.registryMirrors, + imagePullSecrets: c.imagePullSecrets, + } +} + +func (c *containerImageResolver) WithRegistryMirrors(registryMirrors map[string]string) ContainerImageResolver { + return &containerImageResolver{ + logger: c.logger, + resolveForOpenShift: c.resolveForOpenShift, + imageCacher: c.imageCacher, + kubeClient: c.kubeClient, + operatorPodName: c.operatorPodName, + operatorPodNamespace: c.operatorPodNamespace, + imageRegistry: c.imageRegistry, + registryMirrors: registryMirrors, + imagePullSecrets: c.imagePullSecrets, + } +} + +func (c *containerImageResolver) WithImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) ContainerImageResolver { + return &containerImageResolver{ + logger: c.logger, + resolveForOpenShift: c.resolveForOpenShift, + imageCacher: c.imageCacher, + kubeClient: c.kubeClient, + operatorPodName: c.operatorPodName, + operatorPodNamespace: c.operatorPodNamespace, + imageRegistry: c.imageRegistry, + registryMirrors: c.registryMirrors, + imagePullSecrets: imagePullSecrets, + } +} + func userImageOrDefault(defaultImage, defaultTag, userImage, userTag, userDigest string) string { image := defaultImage if userImage != "" { diff --git a/pkg/utils/mondoo/container_image_resolver_test.go b/pkg/utils/mondoo/container_image_resolver_test.go index 16260f9f0..b55584718 100644 --- a/pkg/utils/mondoo/container_image_resolver_test.go +++ b/pkg/utils/mondoo/container_image_resolver_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/stretchr/testify/suite" @@ -16,6 +17,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "go.mondoo.com/mondoo-operator/pkg/imagecache" ) type ContainerImageResolverSuite struct { @@ -33,6 +36,10 @@ func (f *fakeCacher) GetImage(img string) (string, error) { return f.fakeGetImage(img) } +func (f *fakeCacher) WithAuth(keychain authn.Keychain) imagecache.ImageCacher { + return f // Return itself since we don't need auth in tests +} + func NewFakeCacher(f func(string) (string, error)) *fakeCacher { return &fakeCacher{ fakeGetImage: f, diff --git a/pkg/utils/mondoo/fake/container_image_resolver.go b/pkg/utils/mondoo/fake/container_image_resolver.go index 5d77a9811..8bfefde8a 100644 --- a/pkg/utils/mondoo/fake/container_image_resolver.go +++ b/pkg/utils/mondoo/fake/container_image_resolver.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + corev1 "k8s.io/api/core/v1" + "go.mondoo.com/mondoo-operator/pkg/utils/mondoo" ) @@ -65,3 +67,15 @@ func (c *ContainerImageResolverMock) MondooOperatorImage(ctx context.Context, us } return fmt.Sprintf("%s:%s", mondoo.MondooOperatorImage, mondoo.MondooOperatorTag), nil } + +func (c *noOpContainerImageResolver) WithImageRegistry(imageRegistry string) mondoo.ContainerImageResolver { + return c +} + +func (c *noOpContainerImageResolver) WithRegistryMirrors(registryMirrors map[string]string) mondoo.ContainerImageResolver { + return c +} + +func (c *noOpContainerImageResolver) WithImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) mondoo.ContainerImageResolver { + return c +} diff --git a/pkg/utils/mondoo/integration.go b/pkg/utils/mondoo/integration.go index 7a773a06e..f5064e29d 100644 --- a/pkg/utils/mondoo/integration.go +++ b/pkg/utils/mondoo/integration.go @@ -17,6 +17,7 @@ func IntegrationCheckIn( sa mondooclient.ServiceAccountCredentials, mondooClientBuilder MondooClientBuilder, httpProxy *string, + noProxy *string, logger logr.Logger, ) error { token, err := GenerateTokenFromServiceAccount(sa, logger) @@ -28,6 +29,7 @@ func IntegrationCheckIn( ApiEndpoint: sa.ApiEndpoint, Token: token, HttpProxy: httpProxy, + NoProxy: noProxy, }) if err != nil { return err diff --git a/pkg/utils/mondoo/token_exchange.go b/pkg/utils/mondoo/token_exchange.go index 3932a07e3..24fbe048b 100644 --- a/pkg/utils/mondoo/token_exchange.go +++ b/pkg/utils/mondoo/token_exchange.go @@ -26,7 +26,7 @@ type MondooClientBuilder func(mondooclient.MondooClientOptions) (mondooclient.Mo // CreateServiceAccountFromToken will take the provided Mondoo token and exchange it with the Mondoo API // for a long lived Mondoo ServiceAccount -func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client, mondooClientBuilder MondooClientBuilder, withConsoleIntegration bool, serviceAccountSecret types.NamespacedName, tokenSecretData string, httpProxy *string, log logr.Logger) error { +func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client, mondooClientBuilder MondooClientBuilder, withConsoleIntegration bool, serviceAccountSecret types.NamespacedName, tokenSecretData string, httpProxy *string, noProxy *string, log logr.Logger) error { jwtString := strings.TrimSpace(tokenSecretData) parser := &jwt.Parser{} @@ -48,6 +48,7 @@ func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client ApiEndpoint: fmt.Sprintf("%v", apiEndpoint), Token: jwtString, HttpProxy: httpProxy, + NoProxy: noProxy, } mClient, err := mondooClientBuilder(opts) @@ -98,7 +99,7 @@ func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client // No easy way to retry this one-off CheckIn(). An error on initial CheckIn() // means we'll just retry on the regularly scheduled interval via the integration controller - _ = performInitialCheckIn(ctx, mondooClientBuilder, integrationMrn, *resp.Creds, httpProxy, log) + _ = performInitialCheckIn(ctx, mondooClientBuilder, integrationMrn, *resp.Creds, httpProxy, noProxy, log) } else { // Do a vanilla token-for-service-account exchange resp, err := mClient.ExchangeRegistrationToken(ctx, &mondooclient.ExchangeRegistrationTokenInput{ @@ -125,8 +126,8 @@ func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client return nil } -func performInitialCheckIn(ctx context.Context, mondooClientBuilder MondooClientBuilder, integrationMrn string, sa mondooclient.ServiceAccountCredentials, httpProxy *string, logger logr.Logger) error { - if err := IntegrationCheckIn(ctx, integrationMrn, sa, mondooClientBuilder, httpProxy, logger); err != nil { +func performInitialCheckIn(ctx context.Context, mondooClientBuilder MondooClientBuilder, integrationMrn string, sa mondooclient.ServiceAccountCredentials, httpProxy *string, noProxy *string, logger logr.Logger) error { + if err := IntegrationCheckIn(ctx, integrationMrn, sa, mondooClientBuilder, httpProxy, noProxy, logger); err != nil { logger.Error(err, "initial CheckIn() failed, will CheckIn() periodically", "integrationMRN", integrationMrn) return err } From 2d1c3fe44bcede28b6adffe5d20b70c3777e31da Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Fri, 13 Feb 2026 16:14:56 +0100 Subject: [PATCH 2/9] refactor: improve MondooOperatorConfig quality and documentation - Address code quality issues and extract clone() helper - Make noProxy matching case-insensitive - Add logging for imagePullSecret lookup failures - Default createConfig to true in Helm values - Remove deprecated marker from imageRegistry field - Add tests for KeychainFromSecrets and applyImageRegistry - Add operator config documentation --- api/v1alpha2/mondoooperatorconfig_types.go | 10 +- .../mondoo-operator/templates/deployment.yaml | 18 + .../templates/manager-rbac.yaml | 11 + charts/mondoo-operator/values.yaml | 13 +- .../k8s_v1alpha2_mondoooperatorconfig.yaml | 72 +++ controllers/container_image/resources.go | 13 +- .../integration/integration_controller.go | 2 +- controllers/k8s_scan/deployment_handler.go | 2 + controllers/k8s_scan/resources.go | 20 +- controllers/mondooauditconfig_controller.go | 1 + controllers/nodes/resources.go | 34 +- controllers/resource_watcher/resources.go | 10 +- controllers/status/status_reporter.go | 1 + docs/operator-config.md | 508 ++++++++++++++++++ docs/user-manual.md | 6 + hack/update-helm-crds.sh | 66 +-- pkg/client/common/http.go | 53 +- pkg/client/common/http_test.go | 224 ++++++++ pkg/client/mondooclient/client.go | 3 +- pkg/imagecache/imagecache.go | 50 +- pkg/imagecache/imagecache_test.go | 223 ++++++++ pkg/utils/k8s/proxy.go | 37 ++ pkg/utils/mondoo/container_image_resolver.go | 53 +- .../mondoo/container_image_resolver_test.go | 154 ++++++ .../mondoo/fake/container_image_resolver.go | 12 + pkg/utils/mondoo/integration.go | 2 + pkg/utils/mondoo/token_exchange.go | 9 +- 27 files changed, 1426 insertions(+), 181 deletions(-) create mode 100644 docs/operator-config.md create mode 100644 pkg/client/common/http_test.go create mode 100644 pkg/utils/k8s/proxy.go diff --git a/api/v1alpha2/mondoooperatorconfig_types.go b/api/v1alpha2/mondoooperatorconfig_types.go index b254f0f84..74bbba5da 100644 --- a/api/v1alpha2/mondoooperatorconfig_types.go +++ b/api/v1alpha2/mondoooperatorconfig_types.go @@ -37,18 +37,20 @@ type MondooOperatorConfigSpec struct { // ImagePullSecrets specifies the name of the Secret to use for pulling images for all Mondoo components. // The secret must be of type kubernetes.io/dockerconfigjson. ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` - // ImageRegistry specifies a custom container image registry to use for all Mondoo images. - // This allows using a private registry mirror (e.g., artifactory.example.com/ghcr.io.docker). - // If set, all image references will be prefixed with this registry. - // Deprecated: Use RegistryMirrors for more flexible registry mapping. + // ImageRegistry specifies a custom container image registry prefix for all Mondoo images. + // Use this for simple registry mirrors where all images go to the same mirror. + // Example: "artifactory.example.com/ghcr.io.docker" + // For more complex setups with multiple source registries, use RegistryMirrors instead. ImageRegistry *string `json:"imageRegistry,omitempty"` // RegistryMirrors specifies a mapping of public registries to private mirrors. + // Use this when you need to map different source registries to different mirrors. // The key is the public registry (e.g., "ghcr.io", "docker.io", "quay.io") // and the value is the private mirror (e.g., "artifactory.example.com/ghcr.io.docker"). // Example: // registryMirrors: // ghcr.io: artifactory.example.com/ghcr.io.docker // docker.io: artifactory.example.com/hub.docker.com + // Note: If both ImageRegistry and RegistryMirrors are set, RegistryMirrors takes precedence. RegistryMirrors map[string]string `json:"registryMirrors,omitempty"` // SkipProxyForCnspec disables proxy environment variables for cnspec-based components // (scan-api, container scanning). Use this when the Mondoo API is accessible directly diff --git a/charts/mondoo-operator/templates/deployment.yaml b/charts/mondoo-operator/templates/deployment.yaml index 293ebf6af..ed460e8c3 100644 --- a/charts/mondoo-operator/templates/deployment.yaml +++ b/charts/mondoo-operator/templates/deployment.yaml @@ -31,6 +31,24 @@ spec: fieldPath: metadata.namespace - name: KUBERNETES_CLUSTER_DOMAIN value: {{ quote .Values.kubernetesClusterDomain }} + {{- if .Values.operator.httpProxy }} + - name: HTTP_PROXY + value: {{ .Values.operator.httpProxy | quote }} + - name: http_proxy + value: {{ .Values.operator.httpProxy | quote }} + {{- end }} + {{- if .Values.operator.httpsProxy }} + - name: HTTPS_PROXY + value: {{ .Values.operator.httpsProxy | quote }} + - name: https_proxy + value: {{ .Values.operator.httpsProxy | quote }} + {{- end }} + {{- if .Values.operator.noProxy }} + - name: NO_PROXY + value: {{ .Values.operator.noProxy | quote }} + - name: no_proxy + value: {{ .Values.operator.noProxy | quote }} + {{- end }} image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag | default .Chart.AppVersion }} imagePullPolicy: {{ .Values.controllerManager.manager.imagePullPolicy }} diff --git a/charts/mondoo-operator/templates/manager-rbac.yaml b/charts/mondoo-operator/templates/manager-rbac.yaml index 11e5def60..2694a98ee 100644 --- a/charts/mondoo-operator/templates/manager-rbac.yaml +++ b/charts/mondoo-operator/templates/manager-rbac.yaml @@ -36,6 +36,17 @@ rules: - create - delete - get +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get + - list + - update + - watch - apiGroups: - apps resources: diff --git a/charts/mondoo-operator/values.yaml b/charts/mondoo-operator/values.yaml index 2e480ebae..fe2606111 100644 --- a/charts/mondoo-operator/values.yaml +++ b/charts/mondoo-operator/values.yaml @@ -64,8 +64,9 @@ cleanup: # Mondoo Operator Configuration # These settings are applied to the MondooOperatorConfig custom resource operator: - # Create MondooOperatorConfig resource (set to false on first install, then true on upgrade) - createConfig: false + # Create MondooOperatorConfig resource + # Set to false to skip creating the MondooOperatorConfig CR (e.g., if managing it separately) + createConfig: true # HTTP proxy for outbound connections to Mondoo Platform httpProxy: "" # HTTPS proxy for outbound connections to Mondoo Platform @@ -79,18 +80,20 @@ operator: # imagePullSecrets: # - name: my-registry-secret imagePullSecrets: [] - # Custom image registry prefix for corporate registries (deprecated, use registryMirrors) + # Custom image registry prefix for simple registry mirror setups + # Use this when all Mondoo images should be pulled from the same mirror # Example: "registry.example.com/ghcr.io.docker" # This will rewrite image references like: # ghcr.io/mondoohq/mondoo-operator:v1.0.0 -> registry.example.com/ghcr.io.docker/mondoohq/mondoo-operator:v1.0.0 imageRegistry: "" - # Registry mirrors for mapping public registries to private mirrors - # This is more flexible than imageRegistry as it supports multiple registries + # Registry mirrors for mapping multiple public registries to private mirrors + # Use this when you need different mirrors for different source registries # Example: # registryMirrors: # ghcr.io: registry.example.com/ghcr.io.docker # docker.io: registry.example.com/hub.docker.com # quay.io: registry.example.com/quay.io + # Note: If both imageRegistry and registryMirrors are set, registryMirrors takes precedence registryMirrors: {} # Skip proxy settings for cnspec-based components (scan-api, container scanning) diff --git a/config/samples/k8s_v1alpha2_mondoooperatorconfig.yaml b/config/samples/k8s_v1alpha2_mondoooperatorconfig.yaml index 042190001..97b6e9beb 100644 --- a/config/samples/k8s_v1alpha2_mondoooperatorconfig.yaml +++ b/config/samples/k8s_v1alpha2_mondoooperatorconfig.yaml @@ -1,11 +1,83 @@ # Copyright (c) Mondoo, Inc. # SPDX-License-Identifier: BUSL-1.1 +# MondooOperatorConfig defines cluster-wide settings for the Mondoo Operator. +# This is a cluster-scoped resource - only one instance named "mondoo-operator-config" is allowed. +# See docs/operator-config.md for detailed documentation. + apiVersion: k8s.mondoo.com/v1alpha2 kind: MondooOperatorConfig metadata: name: mondoo-operator-config spec: + # ============================================================================ + # METRICS CONFIGURATION + # ============================================================================ + metrics: + # Enable Prometheus metrics and ServiceMonitor creation enable: false + + # Extra labels to add to metrics-related resources (e.g., ServiceMonitor) + # Use this to match your Prometheus Operator's serviceMonitorSelector + # resourceLabels: + # prometheus: main + # team: security + + # ============================================================================ + # PROXY CONFIGURATION + # ============================================================================ + + # HTTP proxy for outbound HTTP connections to the Mondoo Platform + # httpProxy: "http://proxy.example.com:3128" + + # HTTPS proxy for outbound HTTPS connections to the Mondoo Platform + # httpsProxy: "http://proxy.example.com:3128" + + # Comma-separated list of hosts/CIDRs that should bypass the proxy + # Follows standard NO_PROXY conventions: IPs, CIDRs, domains, domain suffixes (.example.com) + # Recommended entries for Kubernetes: + # noProxy: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,.cluster.local,.svc,localhost,127.0.0.1" + + # Proxy specifically for container image operations (used in container image scanning) + # containerProxy: "http://proxy.example.com:3128" + + # Disable proxy environment variables for cnspec-based components (K8s scanning, container scanning) + # Use this when the Mondoo API is accessible directly without proxy (e.g., internal mirror) + # but other components still need proxy for external access + skipProxyForCnspec: false + + # ============================================================================ + # REGISTRY CONFIGURATION (for air-gapped / private registry environments) + # ============================================================================ + + # Image pull secrets for pulling Mondoo container images from private registries + # The secrets must exist in the mondoo-operator namespace and be of type kubernetes.io/dockerconfigjson + # imagePullSecrets: + # - name: ghcr-credentials + # - name: artifactory-credentials + + # Simple registry mirror: prefix for all Mondoo images + # Use this when all images are mirrored to the same registry location + # Example: "registry.example.com/ghcr.io.docker" + # This rewrites: ghcr.io/mondoohq/cnspec:v1.0.0 -> registry.example.com/ghcr.io.docker/mondoohq/cnspec:v1.0.0 + # imageRegistry: "" + + # Advanced registry mirrors: map different source registries to different mirrors + # Use this when you have separate mirrors for different upstream registries + # Note: If both imageRegistry and registryMirrors are set, registryMirrors takes precedence + # registryMirrors: + # ghcr.io: "artifactory.example.com/ghcr-remote" + # docker.io: "artifactory.example.com/docker-hub-remote" + # quay.io: "artifactory.example.com/quay-remote" + + # ============================================================================ + # BEHAVIOR FLAGS + # ============================================================================ + + # Skip resolving container image digests from upstream registries + # Enable this for: + # - Air-gapped clusters without access to ghcr.io + # - GKE Autopilot or environments with restricted network policies + # - When you want to pin exact image versions without upstream resolution skipContainerResolution: false diff --git a/controllers/container_image/resources.go b/controllers/container_image/resources.go index a7fb7a1d2..d6a6fae84 100644 --- a/controllers/container_image/resources.go +++ b/controllers/container_image/resources.go @@ -49,18 +49,7 @@ func CronJob(image, integrationMrn, clusterUid, privateRegistrySecretName string // Add proxy environment variables from MondooOperatorConfig only if SkipProxyForCnspec is false if !cfg.Spec.SkipProxyForCnspec { - if cfg.Spec.HttpProxy != nil { - envVars = append(envVars, corev1.EnvVar{Name: "HTTP_PROXY", Value: *cfg.Spec.HttpProxy}) - envVars = append(envVars, corev1.EnvVar{Name: "http_proxy", Value: *cfg.Spec.HttpProxy}) - } - if cfg.Spec.HttpsProxy != nil { - envVars = append(envVars, corev1.EnvVar{Name: "HTTPS_PROXY", Value: *cfg.Spec.HttpsProxy}) - envVars = append(envVars, corev1.EnvVar{Name: "https_proxy", Value: *cfg.Spec.HttpsProxy}) - } - if cfg.Spec.NoProxy != nil { - envVars = append(envVars, corev1.EnvVar{Name: "NO_PROXY", Value: *cfg.Spec.NoProxy}) - envVars = append(envVars, corev1.EnvVar{Name: "no_proxy", Value: *cfg.Spec.NoProxy}) - } + envVars = append(envVars, k8s.ProxyEnvVars(cfg)...) } envVars = k8s.MergeEnv(envVars, m.Spec.Containers.Env) diff --git a/controllers/integration/integration_controller.go b/controllers/integration/integration_controller.go index 1bf4e7289..24ad76c44 100644 --- a/controllers/integration/integration_controller.go +++ b/controllers/integration/integration_controller.go @@ -123,7 +123,7 @@ func (r *IntegrationReconciler) processMondooAuditConfig(m v1alpha2.MondooAuditC } } - if err = mondoo.IntegrationCheckIn(r.ctx, integrationMrn, *serviceAccount, r.MondooClientBuilder, config.Spec.HttpProxy, config.Spec.NoProxy, logger); err != nil { + if err = mondoo.IntegrationCheckIn(r.ctx, integrationMrn, *serviceAccount, r.MondooClientBuilder, config.Spec.HttpProxy, config.Spec.HttpsProxy, config.Spec.NoProxy, logger); err != nil { logger.Error(err, "failed to CheckIn() for integration", "integrationMRN", string(integrationMrn)) return err } diff --git a/controllers/k8s_scan/deployment_handler.go b/controllers/k8s_scan/deployment_handler.go index 4ef301259..e52a05553 100644 --- a/controllers/k8s_scan/deployment_handler.go +++ b/controllers/k8s_scan/deployment_handler.go @@ -583,6 +583,8 @@ func (n *DeploymentHandler) performGarbageCollection(ctx context.Context, manage } if n.MondooOperatorConfig != nil { opts.HttpProxy = n.MondooOperatorConfig.Spec.HttpProxy + opts.HttpsProxy = n.MondooOperatorConfig.Spec.HttpsProxy + opts.NoProxy = n.MondooOperatorConfig.Spec.NoProxy } mondooClient, err := n.MondooClientBuilder(opts) diff --git a/controllers/k8s_scan/resources.go b/controllers/k8s_scan/resources.go index ddb7cda66..954ae8a26 100644 --- a/controllers/k8s_scan/resources.go +++ b/controllers/k8s_scan/resources.go @@ -450,6 +450,11 @@ func ExternalClusterCronJob(image string, cluster v1alpha2.ExternalCluster, m *v }, } + // Add imagePullSecrets from MondooOperatorConfig + if len(cfg.Spec.ImagePullSecrets) > 0 { + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = cfg.Spec.ImagePullSecrets + } + // Add private registry pull secrets if configured if cluster.PrivateRegistriesPullSecretRef != nil && cluster.PrivateRegistriesPullSecretRef.Name != "" { cronjob.Spec.JobTemplate.Spec.Template.Spec.Volumes = append(cronjob.Spec.JobTemplate.Spec.Template.Spec.Volumes, corev1.Volume{ @@ -1028,18 +1033,9 @@ func ExternalClusterInventory(integrationMRN, operatorClusterUID string, cluster func buildEnvVars(cfg v1alpha2.MondooOperatorConfig) []corev1.EnvVar { envVars := feature_flags.AllFeatureFlagsAsEnv() - // Add proxy environment variables from MondooOperatorConfig - if cfg.Spec.HttpProxy != nil { - envVars = append(envVars, corev1.EnvVar{Name: "HTTP_PROXY", Value: *cfg.Spec.HttpProxy}) - envVars = append(envVars, corev1.EnvVar{Name: "http_proxy", Value: *cfg.Spec.HttpProxy}) - } - if cfg.Spec.HttpsProxy != nil { - envVars = append(envVars, corev1.EnvVar{Name: "HTTPS_PROXY", Value: *cfg.Spec.HttpsProxy}) - envVars = append(envVars, corev1.EnvVar{Name: "https_proxy", Value: *cfg.Spec.HttpsProxy}) - } - if cfg.Spec.NoProxy != nil { - envVars = append(envVars, corev1.EnvVar{Name: "NO_PROXY", Value: *cfg.Spec.NoProxy}) - envVars = append(envVars, corev1.EnvVar{Name: "no_proxy", Value: *cfg.Spec.NoProxy}) + // Add proxy environment variables only if not skipped for cnspec components + if !cfg.Spec.SkipProxyForCnspec { + envVars = append(envVars, k8s.ProxyEnvVars(cfg)...) } return envVars diff --git a/controllers/mondooauditconfig_controller.go b/controllers/mondooauditconfig_controller.go index c716ba401..7043203c3 100644 --- a/controllers/mondooauditconfig_controller.go +++ b/controllers/mondooauditconfig_controller.go @@ -502,6 +502,7 @@ func (r *MondooAuditConfigReconciler) exchangeTokenForServiceAccount(ctx context client.ObjectKeyFromObject(mondooCredsSecret), tokenData, cfg.Spec.HttpProxy, + cfg.Spec.HttpsProxy, cfg.Spec.NoProxy, log) } diff --git a/controllers/nodes/resources.go b/controllers/nodes/resources.go index 3524cad74..d381d5960 100644 --- a/controllers/nodes/resources.go +++ b/controllers/nodes/resources.go @@ -37,24 +37,6 @@ const ( ignoreAnnotationValue = "ignore" ) -// buildProxyEnvVars builds proxy environment variables from MondooOperatorConfig -func buildProxyEnvVars(cfg v1alpha2.MondooOperatorConfig) []corev1.EnvVar { - var proxyEnvVars []corev1.EnvVar - if cfg.Spec.HttpProxy != nil { - proxyEnvVars = append(proxyEnvVars, corev1.EnvVar{Name: "HTTP_PROXY", Value: *cfg.Spec.HttpProxy}) - proxyEnvVars = append(proxyEnvVars, corev1.EnvVar{Name: "http_proxy", Value: *cfg.Spec.HttpProxy}) - } - if cfg.Spec.HttpsProxy != nil { - proxyEnvVars = append(proxyEnvVars, corev1.EnvVar{Name: "HTTPS_PROXY", Value: *cfg.Spec.HttpsProxy}) - proxyEnvVars = append(proxyEnvVars, corev1.EnvVar{Name: "https_proxy", Value: *cfg.Spec.HttpsProxy}) - } - if cfg.Spec.NoProxy != nil { - proxyEnvVars = append(proxyEnvVars, corev1.EnvVar{Name: "NO_PROXY", Value: *cfg.Spec.NoProxy}) - proxyEnvVars = append(proxyEnvVars, corev1.EnvVar{Name: "no_proxy", Value: *cfg.Spec.NoProxy}) - } - return proxyEnvVars -} - // CronJob creates a CronJob for node scanning func CronJob(image string, node corev1.Node, m *v1alpha2.MondooAuditConfig, isOpenshift bool, cfg v1alpha2.MondooOperatorConfig) *batchv1.CronJob { ls := NodeScanningLabels(*m) @@ -64,11 +46,15 @@ func CronJob(image string, node corev1.Node, m *v1alpha2.MondooAuditConfig, isOp "--inventory-template", "/etc/opt/mondoo/inventory_template.yml", } - if cfg.Spec.HttpProxy != nil { + // Add API proxy if configured (respect SkipProxyForCnspec since node scanning uses cnspec) + if !cfg.Spec.SkipProxyForCnspec && cfg.Spec.HttpProxy != nil { cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) } - proxyEnvVars := buildProxyEnvVars(cfg) + var proxyEnvVars []corev1.EnvVar + if !cfg.Spec.SkipProxyForCnspec { + proxyEnvVars = k8s.ProxyEnvVars(cfg) + } containerResources := k8s.ResourcesRequirementsWithDefaults(m.Spec.Nodes.Resources, k8s.DefaultNodeScanningResources) gcLimit := gomemlimit.CalculateGoMemLimit(containerResources) @@ -204,11 +190,15 @@ func DaemonSet(m v1alpha2.MondooAuditConfig, isOpenshift bool, image string, cfg "--inventory-template", "/etc/opt/mondoo/inventory_template.yml", "--timer", fmt.Sprintf("%d", m.Spec.Nodes.IntervalTimer), } - if cfg.Spec.HttpProxy != nil { + // Add API proxy if configured (respect SkipProxyForCnspec since node scanning uses cnspec) + if !cfg.Spec.SkipProxyForCnspec && cfg.Spec.HttpProxy != nil { cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) } - proxyEnvVars := buildProxyEnvVars(cfg) + var proxyEnvVars []corev1.EnvVar + if !cfg.Spec.SkipProxyForCnspec { + proxyEnvVars = k8s.ProxyEnvVars(cfg) + } containerResources := k8s.ResourcesRequirementsWithDefaults(m.Spec.Nodes.Resources, k8s.DefaultNodeScanningResources) gcLimit := gomemlimit.CalculateGoMemLimit(containerResources) diff --git a/controllers/resource_watcher/resources.go b/controllers/resource_watcher/resources.go index f5204d646..9319902c4 100644 --- a/controllers/resource_watcher/resources.go +++ b/controllers/resource_watcher/resources.go @@ -91,8 +91,8 @@ func Deployment(image, integrationMRN, clusterUID string, m *v1alpha2.MondooAudi cmd = append(cmd, "--namespaces-exclude", strings.Join(m.Spec.Filtering.Namespaces.Exclude, ",")) } - // Add API proxy if configured - if cfg.Spec.HttpProxy != nil { + // Add API proxy if configured (respect SkipProxyForCnspec since resource watcher uses cnspec) + if !cfg.Spec.SkipProxyForCnspec && cfg.Spec.HttpProxy != nil { cmd = append(cmd, "--api-proxy", *cfg.Spec.HttpProxy) } @@ -102,6 +102,11 @@ func Deployment(image, integrationMRN, clusterUID string, m *v1alpha2.MondooAudi envVars := feature_flags.AllFeatureFlagsAsEnv() envVars = append(envVars, corev1.EnvVar{Name: "MONDOO_AUTO_UPDATE", Value: "false"}) + // Add proxy environment variables if not skipped for cnspec components + if !cfg.Spec.SkipProxyForCnspec { + envVars = append(envVars, k8s.ProxyEnvVars(cfg)...) + } + // Add custom scanner env vars envVars = append(envVars, m.Spec.Scanner.Env...) @@ -155,6 +160,7 @@ func Deployment(image, integrationMRN, clusterUID string, m *v1alpha2.MondooAudi TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, + ImagePullSecrets: cfg.Spec.ImagePullSecrets, ServiceAccountName: m.Spec.Scanner.ServiceAccountName, Volumes: []corev1.Volume{ { diff --git a/controllers/status/status_reporter.go b/controllers/status/status_reporter.go index 6c2da3841..835d40e98 100644 --- a/controllers/status/status_reporter.go +++ b/controllers/status/status_reporter.go @@ -82,6 +82,7 @@ func (r *StatusReporter) Report(ctx context.Context, m v1alpha2.MondooAuditConfi ApiEndpoint: serviceAccount.ApiEndpoint, Token: token, HttpProxy: cfg.Spec.HttpProxy, + HttpsProxy: cfg.Spec.HttpsProxy, NoProxy: cfg.Spec.NoProxy, }) if err != nil { diff --git a/docs/operator-config.md b/docs/operator-config.md new file mode 100644 index 000000000..e4ec117f4 --- /dev/null +++ b/docs/operator-config.md @@ -0,0 +1,508 @@ +# MondooOperatorConfig Guide + +This guide explains how to configure the Mondoo Operator for enterprise environments including corporate proxies, air-gapped clusters, and private container registries. + +- [MondooOperatorConfig Guide](#mondoooperatorconfig-guide) + - [Overview](#overview) + - [Quick Start](#quick-start) + - [Using Helm](#using-helm) + - [Using kubectl](#using-kubectl) + - [Configuration Reference](#configuration-reference) + - [Use Cases](#use-cases) + - [Corporate Proxy Configuration](#corporate-proxy-configuration) + - [Air-Gapped / Disconnected Clusters](#air-gapped--disconnected-clusters) + - [Private Registry Authentication](#private-registry-authentication) + - [GKE Autopilot / Restricted Environments](#gke-autopilot--restricted-environments) + - [Metrics and Monitoring](#metrics-and-monitoring) + - [How Configuration Flows to Components](#how-configuration-flows-to-components) + - [Troubleshooting](#troubleshooting) + - [Helm Configuration Reference](#helm-configuration-reference) + +## Overview + +`MondooOperatorConfig` is a **cluster-scoped** custom resource that configures operator-wide settings. Unlike `MondooAuditConfig` (which defines what to scan), `MondooOperatorConfig` defines how the operator itself behaves across all scanning workloads. + +Key characteristics: + +- **Cluster-scoped**: One instance applies to the entire cluster +- **Fixed name**: Must be named `mondoo-operator-config` +- **Single instance**: Only one `MondooOperatorConfig` is allowed per cluster +- **Applies globally**: Settings affect all `MondooAuditConfig` resources + +**Relationship to MondooAuditConfig:** + +| Resource | Scope | Purpose | +|----------|-------|---------| +| `MondooOperatorConfig` | Cluster | Operator-wide settings (proxies, registries, metrics) | +| `MondooAuditConfig` | Namespace | What to scan (nodes, K8s resources, containers) | + +## Quick Start + +### Using Helm + +The simplest way to configure `MondooOperatorConfig` is through Helm values: + +```bash +helm install mondoo-operator mondoo/mondoo-operator \ + --namespace mondoo-operator \ + --create-namespace \ + --set operator.httpProxy="http://proxy.example.com:3128" \ + --set operator.httpsProxy="http://proxy.example.com:3128" +``` + +### Using kubectl + +Apply a `MondooOperatorConfig` resource directly: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + metrics: + enable: true +``` + +```bash +kubectl apply -f mondoo-operator-config.yaml +``` + +## Configuration Reference + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `metrics.enable` | bool | `false` | Enable Prometheus metrics and ServiceMonitor creation | +| `metrics.resourceLabels` | map[string]string | `{}` | Extra labels to add to metrics-related resources (e.g., ServiceMonitor) | +| `httpProxy` | string | `""` | HTTP proxy URL for outbound connections | +| `httpsProxy` | string | `""` | HTTPS proxy URL for outbound connections | +| `noProxy` | string | `""` | Comma-separated list of hosts/CIDRs that bypass the proxy | +| `containerProxy` | string | `""` | Proxy for container image operations | +| `imagePullSecrets` | []LocalObjectReference | `[]` | Secrets for pulling Mondoo container images | +| `imageRegistry` | string | `""` | Custom registry prefix for all Mondoo images (simple mirror) | +| `registryMirrors` | map[string]string | `{}` | Map of public registries to private mirrors | +| `skipContainerResolution` | bool | `false` | Skip resolving container image digests from upstream | +| `skipProxyForCnspec` | bool | `false` | Disable proxy settings for cnspec-based components | + +## Use Cases + +### Corporate Proxy Configuration + +Configure the operator to route traffic through your corporate proxy: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + httpProxy: "http://proxy.example.com:3128" + httpsProxy: "http://proxy.example.com:3128" + noProxy: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,.cluster.local,.svc,localhost,127.0.0.1" +``` + +**noProxy format:** + +The `noProxy` field accepts a comma-separated list following the same conventions as the standard `NO_PROXY` environment variable: + +| Pattern | Description | Example | +|---------|-------------|---------| +| IP address | Exact IP match | `192.168.1.1` | +| CIDR notation | IP range | `10.0.0.0/8` | +| Domain | Exact domain match | `internal.example.com` | +| Domain suffix | Matches domain and subdomains (leading `.`) | `.example.com` | +| Wildcard | Matches all hosts (use carefully) | `*` | + +**Recommended noProxy entries for Kubernetes:** + +``` +10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,.cluster.local,.svc,localhost,127.0.0.1 +``` + +This bypasses proxy for: +- Private IP ranges (pod and service CIDRs) +- Kubernetes DNS domains +- Localhost connections + +### Air-Gapped / Disconnected Clusters + +For clusters without internet access, configure the operator to use your internal registry: + +**Option 1: Simple registry mirror (all images from one mirror)** + +Use `imageRegistry` when all Mondoo images are mirrored to the same registry: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + # Rewrites: ghcr.io/mondoohq/cnspec:latest -> registry.example.com/ghcr.io.docker/mondoohq/cnspec:latest + imageRegistry: "registry.example.com/ghcr.io.docker" + # Skip attempts to resolve latest image digests from upstream + skipContainerResolution: true + # Image pull credentials + imagePullSecrets: + - name: registry-credentials +``` + +**Option 2: Multiple registry mirrors (different mirrors per source)** + +Use `registryMirrors` when different source registries map to different mirrors: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + registryMirrors: + ghcr.io: "artifactory.example.com/ghcr-remote" + docker.io: "artifactory.example.com/docker-hub-remote" + quay.io: "artifactory.example.com/quay-remote" + skipContainerResolution: true + imagePullSecrets: + - name: artifactory-credentials +``` + +> **Note:** If both `imageRegistry` and `registryMirrors` are set, `registryMirrors` takes precedence. + +**Step-by-step air-gapped setup:** + +1. **Mirror the required images** to your internal registry: + - `ghcr.io/mondoohq/mondoo-operator:` + - `ghcr.io/mondoohq/cnspec:` + +2. **Create the image pull secret:** + ```bash + kubectl create secret docker-registry registry-credentials \ + --namespace mondoo-operator \ + --docker-server=registry.example.com \ + --docker-username=user \ + --docker-password=password + ``` + +3. **Apply the MondooOperatorConfig** with your registry settings + +4. **Deploy MondooAuditConfig** as normal - the operator applies registry settings automatically + +### Private Registry Authentication + +Configure credentials for pulling Mondoo images from authenticated registries: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + imagePullSecrets: + - name: ghcr-credentials + - name: docker-hub-credentials +``` + +**Creating the required secrets:** + +```bash +# For GitHub Container Registry +kubectl create secret docker-registry ghcr-credentials \ + --namespace mondoo-operator \ + --docker-server=ghcr.io \ + --docker-username=USERNAME \ + --docker-password=GITHUB_TOKEN + +# For Docker Hub (rate limiting) +kubectl create secret docker-registry docker-hub-credentials \ + --namespace mondoo-operator \ + --docker-server=docker.io \ + --docker-username=USERNAME \ + --docker-password=PASSWORD +``` + +**Which components use imagePullSecrets:** + +The `imagePullSecrets` from `MondooOperatorConfig` are applied to all Mondoo-managed workloads: +- Node scanning DaemonSets +- Kubernetes resource scanning CronJobs +- Container image scanning CronJobs + +> **Note:** These are different from `scanner.privateRegistriesPullSecretRef` in `MondooAuditConfig`, which provides credentials for scanning your application's container images (not for pulling Mondoo's own images). + +### GKE Autopilot / Restricted Environments + +Some managed Kubernetes environments restrict certain operations. Use these settings to work around limitations: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + # Skip image digest resolution (requires outbound HTTPS to ghcr.io) + skipContainerResolution: true + # Don't set proxy env vars for cnspec (useful when API is internal) + skipProxyForCnspec: true +``` + +**When to use `skipContainerResolution`:** + +- Air-gapped clusters without access to ghcr.io +- GKE Autopilot where network policies block registry access +- Environments where you want to pin exact image versions + +**When to use `skipProxyForCnspec`:** + +- Your Mondoo API endpoint is internal (no proxy needed) +- Proxy causes issues with certificate validation +- cnspec components need direct access but other components need proxy + +### Metrics and Monitoring + +Enable Prometheus metrics collection: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + metrics: + enable: true + resourceLabels: + prometheus: main + team: security +``` + +**What happens when metrics are enabled:** + +1. The operator creates a `ServiceMonitor` resource for Prometheus Operator +2. Metrics are exposed on the operator's metrics endpoint (port 8080) +3. The `resourceLabels` are added to the ServiceMonitor for label-based selection + +**Prometheus Operator integration:** + +If you use Prometheus Operator, ensure your Prometheus instance is configured to select the ServiceMonitor. The `resourceLabels` can be used to match your Prometheus's `serviceMonitorSelector`: + +```yaml +# Example Prometheus configuration +apiVersion: monitoring.coreos.com/v1 +kind: Prometheus +metadata: + name: main +spec: + serviceMonitorSelector: + matchLabels: + prometheus: main # Matches the resourceLabel above +``` + +**Available metrics:** + +The operator exports standard controller-runtime metrics including: +- Reconciliation counts and durations +- Work queue depth and latency +- Controller error counts + +## How Configuration Flows to Components + +The following table shows which `MondooOperatorConfig` settings affect which components: + +| Setting | Operator | Node Scanning | K8s Resource Scanning | Container Scanning | +|---------|:--------:|:-------------:|:---------------------:|:------------------:| +| `httpProxy` | | ✓ | ✓ | ✓* | +| `httpsProxy` | | ✓ | ✓ | ✓* | +| `noProxy` | | ✓ | ✓ | ✓* | +| `containerProxy` | | | | ✓ | +| `imagePullSecrets` | | ✓ | ✓ | ✓ | +| `imageRegistry` | ✓ | ✓ | ✓ | ✓ | +| `registryMirrors` | ✓ | ✓ | ✓ | ✓ | +| `skipContainerResolution` | ✓ | | | | +| `skipProxyForCnspec` | | | ✓ | ✓ | +| `metrics.enable` | ✓ | | | | + +*\* Unless `skipProxyForCnspec: true`* + +**Configuration inheritance:** + +``` +MondooOperatorConfig (cluster-wide settings) + │ + ▼ + ┌──────────────┐ + │ Operator │ ◄── Uses imageRegistry, skipContainerResolution, metrics + └──────────────┘ + │ + │ Creates workloads with inherited settings + ▼ + ┌──────────────────────────────────────────────────┐ + │ MondooAuditConfig │ + │ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │ + │ │ Nodes │ │ K8s Scan │ │ Containers │ │ + │ │ DaemonSet │ │ CronJob │ │ CronJob │ │ + │ └────────────┘ └────────────┘ └──────────────┘ │ + │ │ │ │ │ + │ └──────────────┼───────────────┘ │ + │ ▼ │ + │ Proxy, ImagePullSecrets, Registry │ + └──────────────────────────────────────────────────┘ +``` + +## Troubleshooting + +### Images not pulling from private registry + +**Symptoms:** Pods fail with `ImagePullBackOff` or `ErrImagePull` + +**Checklist:** + +1. Verify the secret exists in the `mondoo-operator` namespace: + ```bash + kubectl get secrets -n mondoo-operator + ``` + +2. Verify the secret is correctly formatted: + ```bash + kubectl get secret registry-credentials -n mondoo-operator -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d + ``` + +3. Test the credentials manually: + ```bash + docker login registry.example.com -u user -p password + docker pull registry.example.com/ghcr.io.docker/mondoohq/cnspec:latest + ``` + +4. Check that `imagePullSecrets` references the correct secret name in `MondooOperatorConfig` + +5. Restart the operator to pick up changes: + ```bash + kubectl rollout restart deployment mondoo-operator-controller-manager -n mondoo-operator + ``` + +### Proxy not working for some components + +**Symptoms:** Some scans work, others fail with connection errors + +**Checklist:** + +1. Verify proxy environment variables in the pod: + ```bash + kubectl exec -n mondoo-operator -- env | grep -i proxy + ``` + +2. Check if `skipProxyForCnspec` is accidentally enabled: + ```bash + kubectl get mondoooperatorconfig mondoo-operator-config -o yaml | grep skipProxyForCnspec + ``` + +3. Verify `noProxy` includes Kubernetes internal addresses: + ``` + noProxy: "10.0.0.0/8,.cluster.local,.svc,localhost" + ``` + +4. Test proxy connectivity from within a pod: + ```bash + kubectl run -it --rm proxy-test --image=curlimages/curl -- \ + curl -x http://proxy.example.com:3128 https://api.mondoo.com + ``` + +### MondooOperatorConfig not being applied + +**Symptoms:** Configuration changes don't take effect + +**Checklist:** + +1. Verify the resource name is exactly `mondoo-operator-config`: + ```bash + kubectl get mondoooperatorconfig + ``` + +2. Check for validation errors in the resource: + ```bash + kubectl describe mondoooperatorconfig mondoo-operator-config + ``` + +3. Check operator logs for configuration loading: + ```bash + kubectl logs -n mondoo-operator deployment/mondoo-operator-controller-manager | grep -i config + ``` + +4. Restart the operator to force configuration reload: + ```bash + kubectl rollout restart deployment mondoo-operator-controller-manager -n mondoo-operator + ``` + +### Metrics not appearing in Prometheus + +**Symptoms:** ServiceMonitor created but no metrics in Prometheus + +**Checklist:** + +1. Verify ServiceMonitor exists: + ```bash + kubectl get servicemonitor -n mondoo-operator + ``` + +2. Check ServiceMonitor labels match your Prometheus selector: + ```bash + kubectl get servicemonitor -n mondoo-operator -o yaml | grep -A5 labels + ``` + +3. Verify Prometheus is configured to watch the namespace: + ```bash + kubectl get prometheus -o yaml | grep -A10 serviceMonitorNamespaceSelector + ``` + +4. Check Prometheus targets page for the mondoo-operator target + +5. Verify the metrics endpoint is accessible: + ```bash + kubectl port-forward -n mondoo-operator svc/mondoo-operator-controller-manager-metrics-service 8080:8080 + curl http://localhost:8080/metrics + ``` + +## Helm Configuration Reference + +When using Helm, the following `values.yaml` settings map to `MondooOperatorConfig` fields: + +```yaml +operator: + # Create MondooOperatorConfig resource + createConfig: true + + # Proxy settings + httpProxy: "http://proxy.example.com:3128" + httpsProxy: "http://proxy.example.com:3128" + noProxy: "10.0.0.0/8,.cluster.local" + containerProxy: "" + + # Registry settings + imageRegistry: "registry.example.com/ghcr.io.docker" + registryMirrors: + ghcr.io: "registry.example.com/ghcr-remote" + docker.io: "registry.example.com/docker-hub-remote" + + # Image pull credentials + imagePullSecrets: + - name: registry-credentials + + # Behavior flags + skipContainerResolution: true + skipProxyForCnspec: false +``` + +**Mapping table:** + +| Helm Value | MondooOperatorConfig Field | +|------------|---------------------------| +| `operator.createConfig` | Controls whether CR is created | +| `operator.httpProxy` | `spec.httpProxy` | +| `operator.httpsProxy` | `spec.httpsProxy` | +| `operator.noProxy` | `spec.noProxy` | +| `operator.containerProxy` | `spec.containerProxy` | +| `operator.imageRegistry` | `spec.imageRegistry` | +| `operator.registryMirrors` | `spec.registryMirrors` | +| `operator.imagePullSecrets` | `spec.imagePullSecrets` | +| `operator.skipContainerResolution` | `spec.skipContainerResolution` | +| `operator.skipProxyForCnspec` | `spec.skipProxyForCnspec` | + +> **Note:** Metrics configuration is not currently exposed via Helm values. Apply a `MondooOperatorConfig` directly to enable metrics. diff --git a/docs/user-manual.md b/docs/user-manual.md index 995518270..8924f8682 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -3,6 +3,7 @@ This user manual describes how to install and use the Mondoo Operator. - [User manual](#user-manual) + - [Configuring the Operator](#configuring-the-operator) - [Mondoo Operator Installation](#mondoo-operator-installation) - [Installing with kubectl](#installing-with-kubectl) - [Installing with Helm](#installing-with-helm) @@ -51,6 +52,11 @@ When upgrading from operator versions prior to v12.x, the following changes appl - **High-priority resources by default**: The watcher now only monitors Deployments, DaemonSets, StatefulSets, and ReplicaSets by default (previously watched all resources including Pods and Jobs). To restore the previous behavior, set `watchAllResources: true` in your configuration. +## Configuring the Operator + +For cluster-wide settings like proxies, registry mirrors, and metrics, see the +[MondooOperatorConfig Guide](operator-config.md). + ## Mondoo Operator Installation Install the Mondoo Operator using kubectl, Helm, or Operator Lifecycle Manager. diff --git a/hack/update-helm-crds.sh b/hack/update-helm-crds.sh index 65c3b8680..782b8b538 100755 --- a/hack/update-helm-crds.sh +++ b/hack/update-helm-crds.sh @@ -2,10 +2,9 @@ # Copyright Mondoo, Inc. 2026 # SPDX-License-Identifier: BUSL-1.1 # -# This script updates the CRD templates in the Helm chart from the generated CRDs. -# It applies the necessary transformations to make them Helm-compatible: -# - Adds Helm labels template -# - Replaces webhook namespace with Helm template +# This script updates the CRDs in the Helm chart from the generated CRDs. +# CRDs are placed in charts/mondoo-operator/crds/ which Helm installs +# automatically before other chart resources. # # Usage: ./hack/update-helm-crds.sh @@ -13,58 +12,13 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -CHART_DIR="${ROOT_DIR}/charts/mondoo-operator/templates" +CRD_BASES="${ROOT_DIR}/config/crd/bases" +CHART_CRDS="${ROOT_DIR}/charts/mondoo-operator/crds" -# Check for required tools -if ! command -v yq &> /dev/null; then - echo "Error: yq is required but not installed." - echo "Install with: brew install yq (macOS) or go install github.com/mikefarah/yq/v4@latest" - exit 1 -fi +echo "Copying CRDs from ${CRD_BASES} to ${CHART_CRDS}..." +cp "${CRD_BASES}/k8s.mondoo.com_mondooauditconfigs.yaml" "${CHART_CRDS}/" +cp "${CRD_BASES}/k8s.mondoo.com_mondoooperatorconfigs.yaml" "${CHART_CRDS}/" -if ! command -v kustomize &> /dev/null && [ ! -f "${ROOT_DIR}/bin/kustomize" ]; then - echo "Error: kustomize is required. Run 'make kustomize' first." - exit 1 -fi - -KUSTOMIZE="${ROOT_DIR}/bin/kustomize" -if [ ! -f "$KUSTOMIZE" ]; then - KUSTOMIZE="kustomize" -fi - -echo "Building CRDs with kustomize..." -CRD_OUTPUT=$("$KUSTOMIZE" build "${ROOT_DIR}/config/crd") - -# Process mondooauditconfigs CRD -echo "Processing mondooauditconfigs CRD..." -echo "$CRD_OUTPUT" | yq eval 'select(.metadata.name == "mondooauditconfigs.k8s.mondoo.com")' - | \ - yq eval 'del(.metadata.labels)' - | \ - yq eval '.spec.conversion.webhook.clientConfig.service.namespace = "HELM_NAMESPACE_PLACEHOLDER"' - | \ - sed 's/name: mondooauditconfigs.k8s.mondoo.com/name: mondooauditconfigs.k8s.mondoo.com\n labels:\n {{- include "mondoo-operator.labels" . | nindent 4 }}/' | \ - sed "s/HELM_NAMESPACE_PLACEHOLDER/'{{ .Release.Namespace }}'/" \ - > "${CHART_DIR}/mondooauditconfig-crd.yaml" - -# Process mondoooperatorconfigs CRD -echo "Processing mondoooperatorconfigs CRD..." -echo "$CRD_OUTPUT" | yq eval 'select(.metadata.name == "mondoooperatorconfigs.k8s.mondoo.com")' - | \ - yq eval 'del(.metadata.labels)' - | \ - sed 's/name: mondoooperatorconfigs.k8s.mondoo.com/name: mondoooperatorconfigs.k8s.mondoo.com\n labels:\n {{- include "mondoo-operator.labels" . | nindent 4 }}/' \ - > "${CHART_DIR}/mondoooperatorconfig-crd.yaml" - -# Validate that Helm template directives were injected correctly -echo "Validating Helm template directives..." -for crd_file in "${CHART_DIR}/mondooauditconfig-crd.yaml" "${CHART_DIR}/mondoooperatorconfig-crd.yaml"; do - if ! grep -q 'include "mondoo-operator.labels"' "$crd_file"; then - echo "Error: Helm labels template not found in $(basename "$crd_file")" - exit 1 - fi -done - -if ! grep -q '\.Release\.Namespace' "${CHART_DIR}/mondooauditconfig-crd.yaml"; then - echo "Error: Helm namespace template not found in mondooauditconfig-crd.yaml" - exit 1 -fi - -echo "CRDs updated successfully in ${CHART_DIR}" +echo "CRDs updated successfully in ${CHART_CRDS}" echo "" -echo "Please review the changes with: git diff charts/mondoo-operator/templates/*-crd.yaml" +echo "Please review the changes with: git diff charts/mondoo-operator/crds/" diff --git a/pkg/client/common/http.go b/pkg/client/common/http.go index 95dc0c300..b301d06e8 100644 --- a/pkg/client/common/http.go +++ b/pkg/client/common/http.go @@ -24,10 +24,10 @@ const ( ) func DefaultHttpClient(httpProxy *string, httpTimeout *time.Duration) (http.Client, error) { - return DefaultHttpClientWithNoProxy(httpProxy, nil, httpTimeout) + return DefaultHttpClientWithProxy(httpProxy, nil, nil, httpTimeout) } -func DefaultHttpClientWithNoProxy(httpProxy *string, noProxy *string, httpTimeout *time.Duration) (http.Client, error) { +func DefaultHttpClientWithProxy(httpProxy *string, httpsProxy *string, noProxy *string, httpTimeout *time.Duration) (http.Client, error) { tr := &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ @@ -40,17 +40,33 @@ func DefaultHttpClientWithNoProxy(httpProxy *string, noProxy *string, httpTimeou ExpectContinueTimeout: 1 * time.Second, } - if httpProxy != nil { - urlParsed, err := url.Parse(*httpProxy) - if err != nil { - return http.Client{}, err + if httpProxy != nil || httpsProxy != nil { + var httpProxyURL, httpsProxyURL *url.URL + var err error + if httpProxy != nil { + httpProxyURL, err = url.Parse(*httpProxy) + if err != nil { + return http.Client{}, fmt.Errorf("failed to parse httpProxy: %w", err) + } + } + if httpsProxy != nil { + httpsProxyURL, err = url.Parse(*httpsProxy) + if err != nil { + return http.Client{}, fmt.Errorf("failed to parse httpsProxy: %w", err) + } } - // Create a proxy function that respects noProxy settings + // Create a proxy function that respects noProxy and uses the correct proxy per scheme tr.Proxy = func(req *http.Request) (*url.URL, error) { if noProxy != nil && shouldBypassProxy(req.URL.Host, *noProxy) { return nil, nil // No proxy for this host } - return urlParsed, nil + if req.URL.Scheme == "https" && httpsProxyURL != nil { + return httpsProxyURL, nil + } + if httpProxyURL != nil { + return httpProxyURL, nil + } + return nil, nil } } timeout := defaultHttpTimeout @@ -63,18 +79,22 @@ func DefaultHttpClientWithNoProxy(httpProxy *string, noProxy *string, httpTimeou }, nil } -// shouldBypassProxy checks if the given host should bypass the proxy based on noProxy settings +// shouldBypassProxy checks if the given host should bypass the proxy based on noProxy settings. +// Matching is case-insensitive since DNS names are case-insensitive. func shouldBypassProxy(host string, noProxy string) bool { if noProxy == "" { return false } - // Remove port from host if present + // Remove port from host if present (handles IPv6 correctly) hostWithoutPort := host - if colonIndex := strings.LastIndex(host, ":"); colonIndex != -1 { - hostWithoutPort = host[:colonIndex] + if h, _, err := net.SplitHostPort(host); err == nil { + hostWithoutPort = h } + // Lowercase for case-insensitive comparison (DNS is case-insensitive) + hostLower := strings.ToLower(hostWithoutPort) + // Check each entry in the noProxy list for _, entry := range strings.Split(noProxy, ",") { entry = strings.TrimSpace(entry) @@ -87,9 +107,12 @@ func shouldBypassProxy(host string, noProxy string) bool { return true } + // Lowercase entry for case-insensitive comparison + entryLower := strings.ToLower(entry) + // Handle domain suffix matching (e.g., ".example.com" matches "api.mondoo.example.com") - if strings.HasPrefix(entry, ".") { - if strings.HasSuffix(hostWithoutPort, entry) || hostWithoutPort == entry[1:] { + if strings.HasPrefix(entryLower, ".") { + if strings.HasSuffix(hostLower, entryLower) || hostLower == entryLower[1:] { return true } continue @@ -108,7 +131,7 @@ func shouldBypassProxy(host string, noProxy string) bool { } // Exact match or suffix match - if hostWithoutPort == entry || strings.HasSuffix(hostWithoutPort, "."+entry) { + if hostLower == entryLower || strings.HasSuffix(hostLower, "."+entryLower) { return true } } diff --git a/pkg/client/common/http_test.go b/pkg/client/common/http_test.go new file mode 100644 index 000000000..f6497370c --- /dev/null +++ b/pkg/client/common/http_test.go @@ -0,0 +1,224 @@ +// Copyright Mondoo, Inc. 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package common + +import ( + "testing" +) + +func TestShouldBypassProxy(t *testing.T) { + tests := []struct { + name string + host string + noProxy string + expected bool + }{ + // Empty noProxy + { + name: "empty noProxy returns false", + host: "api.mondoo.com", + noProxy: "", + expected: false, + }, + // Wildcard "*" + { + name: "wildcard matches any host", + host: "api.mondoo.com", + noProxy: "*", + expected: true, + }, + { + name: "wildcard matches localhost", + host: "localhost", + noProxy: "*", + expected: true, + }, + { + name: "wildcard matches IP address", + host: "10.1.2.3", + noProxy: "*", + expected: true, + }, + // Exact match + { + name: "exact match matches", + host: "api.mondoo.com", + noProxy: "api.mondoo.com", + expected: true, + }, + { + name: "exact match does not match different host", + host: "other.mondoo.com", + noProxy: "api.mondoo.com", + expected: false, + }, + { + name: "exact match is case insensitive", + host: "API.MONDOO.COM", + noProxy: "api.mondoo.com", + expected: true, + }, + // Domain suffix with leading dot + { + name: "domain suffix .example.com matches api.example.com", + host: "api.example.com", + noProxy: ".example.com", + expected: true, + }, + { + name: "domain suffix .example.com matches foo.bar.example.com", + host: "foo.bar.example.com", + noProxy: ".example.com", + expected: true, + }, + { + name: "domain suffix .example.com matches example.com exactly", + host: "example.com", + noProxy: ".example.com", + expected: true, + }, + { + name: "domain suffix .example.com does not match notexample.com", + host: "notexample.com", + noProxy: ".example.com", + expected: false, + }, + // Suffix without leading dot + { + name: "suffix example.com matches api.example.com", + host: "api.example.com", + noProxy: "example.com", + expected: true, + }, + { + name: "suffix example.com matches example.com exactly", + host: "example.com", + noProxy: "example.com", + expected: true, + }, + { + name: "suffix example.com does not match notexample.com", + host: "notexample.com", + noProxy: "example.com", + expected: false, + }, + // CIDR notation + { + name: "CIDR 10.0.0.0/8 matches 10.1.2.3", + host: "10.1.2.3", + noProxy: "10.0.0.0/8", + expected: true, + }, + { + name: "CIDR 10.0.0.0/8 matches 10.255.255.255", + host: "10.255.255.255", + noProxy: "10.0.0.0/8", + expected: true, + }, + { + name: "CIDR 10.0.0.0/8 does not match 192.168.1.1", + host: "192.168.1.1", + noProxy: "10.0.0.0/8", + expected: false, + }, + { + name: "CIDR 192.168.0.0/16 matches 192.168.1.1", + host: "192.168.1.1", + noProxy: "192.168.0.0/16", + expected: true, + }, + { + name: "CIDR does not apply to hostnames", + host: "api.mondoo.com", + noProxy: "10.0.0.0/8", + expected: false, + }, + // Host with port + { + name: "host with port strips port before matching", + host: "api.mondoo.com:443", + noProxy: "api.mondoo.com", + expected: true, + }, + { + name: "host with port matches domain suffix", + host: "api.example.com:8080", + noProxy: ".example.com", + expected: true, + }, + { + name: "IP with port matches CIDR", + host: "10.1.2.3:9000", + noProxy: "10.0.0.0/8", + expected: true, + }, + // Multiple entries + { + name: "multiple entries matches first", + host: "localhost", + noProxy: "localhost,.local,10.0.0.0/8", + expected: true, + }, + { + name: "multiple entries matches second", + host: "myhost.local", + noProxy: "localhost,.local,10.0.0.0/8", + expected: true, + }, + { + name: "multiple entries matches third CIDR", + host: "10.1.2.3", + noProxy: "localhost,.local,10.0.0.0/8", + expected: true, + }, + { + name: "multiple entries does not match any", + host: "api.mondoo.com", + noProxy: "localhost,.local,10.0.0.0/8", + expected: false, + }, + // Whitespace handling + { + name: "whitespace around entries is trimmed", + host: "localhost", + noProxy: " localhost , .local ", + expected: true, + }, + { + name: "whitespace in entries is trimmed for domain", + host: "api.local", + noProxy: " localhost , .local ", + expected: true, + }, + { + name: "empty entries after split are ignored", + host: "localhost", + noProxy: "localhost,,,.local", + expected: true, + }, + // Edge cases + { + name: "single localhost entry", + host: "localhost", + noProxy: "localhost", + expected: true, + }, + { + name: "invalid CIDR is ignored", + host: "10.1.2.3", + noProxy: "invalid/cidr", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := shouldBypassProxy(tt.host, tt.noProxy) + if result != tt.expected { + t.Errorf("shouldBypassProxy(%q, %q) = %v, expected %v", + tt.host, tt.noProxy, result, tt.expected) + } + }) + } +} diff --git a/pkg/client/mondooclient/client.go b/pkg/client/mondooclient/client.go index 1f0f37a38..fbd7cb22a 100644 --- a/pkg/client/mondooclient/client.go +++ b/pkg/client/mondooclient/client.go @@ -26,6 +26,7 @@ type MondooClientOptions struct { ApiEndpoint string Token string HttpProxy *string + HttpsProxy *string NoProxy *string HttpTimeout *time.Duration } @@ -38,7 +39,7 @@ type mondooClient struct { func NewClient(opts MondooClientOptions) (MondooClient, error) { opts.ApiEndpoint = strings.TrimRight(opts.ApiEndpoint, "/") - client, err := common.DefaultHttpClientWithNoProxy(opts.HttpProxy, opts.NoProxy, opts.HttpTimeout) + client, err := common.DefaultHttpClientWithProxy(opts.HttpProxy, opts.HttpsProxy, opts.NoProxy, opts.HttpTimeout) if err != nil { return nil, err } diff --git a/pkg/imagecache/imagecache.go b/pkg/imagecache/imagecache.go index c62b6a3b7..380da5e95 100644 --- a/pkg/imagecache/imagecache.go +++ b/pkg/imagecache/imagecache.go @@ -7,6 +7,8 @@ import ( "context" "encoding/base64" "encoding/json" + "fmt" + "strings" "sync" "time" @@ -15,7 +17,9 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" "go.mondoo.com/cnquery/v12/providers/os/connection/container/auth" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" + ctrl "sigs.k8s.io/controller-runtime/pkg/log" ) const ( @@ -148,6 +152,8 @@ type DockerConfigEntry struct { // KeychainFromSecrets creates an authn.Keychain from Kubernetes imagePullSecrets func KeychainFromSecrets(ctx context.Context, kubeClient client.Client, namespace string, secretRefs []corev1.LocalObjectReference) (authn.Keychain, error) { + log := ctrl.Log.WithName("imagecache") + if len(secretRefs) == 0 { return authn.DefaultKeychain, nil } @@ -156,7 +162,11 @@ func KeychainFromSecrets(ctx context.Context, kubeClient client.Client, namespac for _, secretRef := range secretRefs { secret := &corev1.Secret{} if err := kubeClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: secretRef.Name}, secret); err != nil { - continue // Skip secrets that don't exist + if errors.IsNotFound(err) { + log.Info("imagePullSecret not found, skipping", "secret", secretRef.Name, "namespace", namespace) + continue + } + return nil, fmt.Errorf("failed to get imagePullSecret %s/%s: %w", namespace, secretRef.Name, err) } // Handle both .dockerconfigjson and .dockercfg formats @@ -166,11 +176,13 @@ func KeychainFromSecrets(ctx context.Context, kubeClient client.Client, namespac } else if data, ok := secret.Data[".dockercfg"]; ok { configData = data } else { + log.Info("imagePullSecret has no .dockerconfigjson or .dockercfg data, skipping", "secret", secretRef.Name, "namespace", namespace) continue } var config DockerConfigJSON if err := json.Unmarshal(configData, &config); err != nil { + log.Error(err, "failed to parse imagePullSecret data", "secret", secretRef.Name, "namespace", namespace) continue } configs = append(configs, config) @@ -187,13 +199,21 @@ type multiKeychain struct { func (k *multiKeychain) Resolve(resource authn.Resource) (authn.Authenticator, error) { registry := resource.RegistryStr() + // Build candidate keys to look up in the docker config auths map. + // Docker configs can use various formats: "registry.io", "https://registry.io", + // "https://registry.io/v1/", "https://registry.io/v2/", etc. + candidates := []string{ + registry, + "https://" + registry, + "https://" + registry + "/v1/", + "https://" + registry + "/v2/", + } + for _, config := range k.configs { - if entry, ok := config.Auths[registry]; ok { - return resolveAuth(entry) - } - // Also try with https:// prefix - if entry, ok := config.Auths["https://"+registry]; ok { - return resolveAuth(entry) + for _, candidate := range candidates { + if entry, ok := config.Auths[candidate]; ok { + return resolveAuth(entry) + } } } @@ -204,17 +224,15 @@ func resolveAuth(entry DockerConfigEntry) (authn.Authenticator, error) { if entry.Auth != "" { decoded, err := base64.StdEncoding.DecodeString(entry.Auth) if err != nil { - return authn.Anonymous, nil + return nil, fmt.Errorf("failed to decode auth field: %w", err) } // Auth is base64 encoded "username:password" - parts := string(decoded) - for i, c := range parts { - if c == ':' { - return authn.FromConfig(authn.AuthConfig{ - Username: parts[:i], - Password: parts[i+1:], - }), nil - } + parts := strings.SplitN(string(decoded), ":", 2) + if len(parts) == 2 { + return authn.FromConfig(authn.AuthConfig{ + Username: parts[0], + Password: parts[1], + }), nil } } diff --git a/pkg/imagecache/imagecache_test.go b/pkg/imagecache/imagecache_test.go index 15ae77342..2b517d41f 100644 --- a/pkg/imagecache/imagecache_test.go +++ b/pkg/imagecache/imagecache_test.go @@ -4,12 +4,20 @@ package imagecache import ( + "context" + "encoding/base64" + "encoding/json" "fmt" "testing" "time" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) const ( @@ -95,3 +103,218 @@ func TestCache(t *testing.T) { }) } } + +func TestKeychainFromSecrets(t *testing.T) { + tests := []struct { + name string + secrets []corev1.Secret + secretRefs []corev1.LocalObjectReference + registry string + expectUsername string + expectPassword string + expectAnon bool + }{ + { + name: "empty secret refs returns default keychain", + secrets: nil, + secretRefs: nil, + expectAnon: true, + }, + { + name: "secret not found is skipped", + secretRefs: []corev1.LocalObjectReference{ + {Name: "nonexistent-secret"}, + }, + expectAnon: true, + }, + { + name: "dockerconfigjson with auth field", + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret", + Namespace: "test-ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": mustMarshalDockerConfig(t, DockerConfigJSON{ + Auths: map[string]DockerConfigEntry{ + "ghcr.io": { + Auth: base64.StdEncoding.EncodeToString([]byte("myuser:mypass")), + }, + }, + }), + }, + }, + }, + secretRefs: []corev1.LocalObjectReference{ + {Name: "my-secret"}, + }, + registry: "ghcr.io", + expectUsername: "myuser", + expectPassword: "mypass", + }, + { + name: "dockerconfigjson with username/password fields", + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret", + Namespace: "test-ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": mustMarshalDockerConfig(t, DockerConfigJSON{ + Auths: map[string]DockerConfigEntry{ + // Use index.docker.io as that's what go-containerregistry normalizes docker.io to + "index.docker.io": { + Username: "dockeruser", + Password: "dockerpass", + }, + }, + }), + }, + }, + }, + secretRefs: []corev1.LocalObjectReference{ + {Name: "my-secret"}, + }, + registry: "docker.io", + expectUsername: "dockeruser", + expectPassword: "dockerpass", + }, + { + name: "multiple secrets - first match wins", + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "secret1", + Namespace: "test-ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": mustMarshalDockerConfig(t, DockerConfigJSON{ + Auths: map[string]DockerConfigEntry{ + "ghcr.io": { + Username: "user1", + Password: "pass1", + }, + }, + }), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "secret2", + Namespace: "test-ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": mustMarshalDockerConfig(t, DockerConfigJSON{ + Auths: map[string]DockerConfigEntry{ + "ghcr.io": { + Username: "user2", + Password: "pass2", + }, + }, + }), + }, + }, + }, + secretRefs: []corev1.LocalObjectReference{ + {Name: "secret1"}, + {Name: "secret2"}, + }, + registry: "ghcr.io", + expectUsername: "user1", + expectPassword: "pass1", + }, + { + name: "registry with https prefix in secret", + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret", + Namespace: "test-ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": mustMarshalDockerConfig(t, DockerConfigJSON{ + Auths: map[string]DockerConfigEntry{ + "https://ghcr.io": { + Username: "httpsuser", + Password: "httpspass", + }, + }, + }), + }, + }, + }, + secretRefs: []corev1.LocalObjectReference{ + {Name: "my-secret"}, + }, + registry: "ghcr.io", + expectUsername: "httpsuser", + expectPassword: "httpspass", + }, + { + name: "secret with wrong data key is skipped", + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret", + Namespace: "test-ns", + }, + Data: map[string][]byte{ + "wrongkey": []byte("somedata"), + }, + }, + }, + secretRefs: []corev1.LocalObjectReference{ + {Name: "my-secret"}, + }, + expectAnon: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Build fake client with secrets + builder := fake.NewClientBuilder() + for i := range tt.secrets { + builder = builder.WithObjects(&tt.secrets[i]) + } + kubeClient := builder.Build() + + keychain, err := KeychainFromSecrets(context.Background(), kubeClient, "test-ns", tt.secretRefs) + require.NoError(t, err) + + if tt.registry == "" { + return + } + + // Create a fake resource to resolve auth for + ref, err := name.ParseReference(tt.registry + "/test/image:latest") + require.NoError(t, err) + + auth, err := keychain.Resolve(ref.Context()) + require.NoError(t, err) + + if tt.expectAnon { + assert.Equal(t, authn.Anonymous, auth) + return + } + + authConfig, err := auth.Authorization() + require.NoError(t, err) + assert.Equal(t, tt.expectUsername, authConfig.Username) + assert.Equal(t, tt.expectPassword, authConfig.Password) + }) + } +} + +func mustMarshalDockerConfig(t *testing.T, config DockerConfigJSON) []byte { + data, err := json.Marshal(config) + require.NoError(t, err) + return data +} diff --git a/pkg/utils/k8s/proxy.go b/pkg/utils/k8s/proxy.go new file mode 100644 index 000000000..49a48fa84 --- /dev/null +++ b/pkg/utils/k8s/proxy.go @@ -0,0 +1,37 @@ +// Copyright Mondoo, Inc. 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package k8s + +import ( + corev1 "k8s.io/api/core/v1" + + "go.mondoo.com/mondoo-operator/api/v1alpha2" +) + +// ProxyEnvVars returns environment variables for HTTP/HTTPS proxy configuration. +// It sets both uppercase and lowercase variants for compatibility with different tools. +func ProxyEnvVars(cfg v1alpha2.MondooOperatorConfig) []corev1.EnvVar { + var envVars []corev1.EnvVar + + if cfg.Spec.HttpProxy != nil { + envVars = append(envVars, + corev1.EnvVar{Name: "HTTP_PROXY", Value: *cfg.Spec.HttpProxy}, + corev1.EnvVar{Name: "http_proxy", Value: *cfg.Spec.HttpProxy}, + ) + } + if cfg.Spec.HttpsProxy != nil { + envVars = append(envVars, + corev1.EnvVar{Name: "HTTPS_PROXY", Value: *cfg.Spec.HttpsProxy}, + corev1.EnvVar{Name: "https_proxy", Value: *cfg.Spec.HttpsProxy}, + ) + } + if cfg.Spec.NoProxy != nil { + envVars = append(envVars, + corev1.EnvVar{Name: "NO_PROXY", Value: *cfg.Spec.NoProxy}, + corev1.EnvVar{Name: "no_proxy", Value: *cfg.Spec.NoProxy}, + ) + } + + return envVars +} diff --git a/pkg/utils/mondoo/container_image_resolver.go b/pkg/utils/mondoo/container_image_resolver.go index 43d54b003..c6c4bbe34 100644 --- a/pkg/utils/mondoo/container_image_resolver.go +++ b/pkg/utils/mondoo/container_image_resolver.go @@ -45,12 +45,12 @@ type ContainerImageResolver interface { // When userDigest is specified, it takes precedence over userTag. MondooOperatorImage(ctx context.Context, userImage, userTag, userDigest string, skipImageResolution bool) (string, error) - // WithImageRegistry returns a new ContainerImageResolver that uses the specified image registry. - // This allows rewriting image references to use a custom registry (e.g., corporate Artifactory mirror). - // Deprecated: Use WithRegistryMirrors for more flexible registry mapping. + // WithImageRegistry returns a new ContainerImageResolver that uses the specified image registry prefix. + // Use this for simple registry mirrors where all images go to the same mirror. WithImageRegistry(imageRegistry string) ContainerImageResolver // WithRegistryMirrors returns a new ContainerImageResolver that uses the specified registry mirrors. + // Use this when you need to map different source registries to different mirrors. // The mirrors map public registries to private mirrors (e.g., "ghcr.io" -> "artifactory.example.com/ghcr.io.docker"). WithRegistryMirrors(registryMirrors map[string]string) ContainerImageResolver @@ -110,7 +110,7 @@ func (c *containerImageResolver) CnspecImage(userImage, userTag, userDigest stri skipImageResolution = true } - return c.resolveImage(image, skipImageResolution) + return c.resolveImage(context.Background(), image, skipImageResolution) } func (c *containerImageResolver) MondooOperatorImage(ctx context.Context, userImage, userTag, userDigest string, skipImageResolution bool) (string, error) { @@ -144,10 +144,10 @@ func (c *containerImageResolver) MondooOperatorImage(ctx context.Context, userIm skipImageResolution = true } - return c.resolveImage(image, skipImageResolution) + return c.resolveImage(ctx, image, skipImageResolution) } -func (c *containerImageResolver) resolveImage(image string, skipImageResolution bool) (string, error) { +func (c *containerImageResolver) resolveImage(ctx context.Context, image string, skipImageResolution bool) (string, error) { // Apply custom image registry prefix if configured image = c.applyImageRegistry(image) @@ -158,7 +158,7 @@ func (c *containerImageResolver) resolveImage(image string, skipImageResolution // Apply authentication if imagePullSecrets are configured cacher := c.imageCacher if len(c.imagePullSecrets) > 0 { - keychain, err := imagecache.KeychainFromSecrets(context.Background(), c.kubeClient, c.operatorPodNamespace, c.imagePullSecrets) + keychain, err := imagecache.KeychainFromSecrets(ctx, c.kubeClient, c.operatorPodNamespace, c.imagePullSecrets) if err == nil { cacher = cacher.WithAuth(keychain) } @@ -236,7 +236,8 @@ func splitImageParts(image string) imageParts { return imageParts{registry: "", repositoryWithTag: image} } -func (c *containerImageResolver) WithImageRegistry(imageRegistry string) ContainerImageResolver { +// clone creates a shallow copy of the resolver +func (c *containerImageResolver) clone() *containerImageResolver { return &containerImageResolver{ logger: c.logger, resolveForOpenShift: c.resolveForOpenShift, @@ -244,38 +245,28 @@ func (c *containerImageResolver) WithImageRegistry(imageRegistry string) Contain kubeClient: c.kubeClient, operatorPodName: c.operatorPodName, operatorPodNamespace: c.operatorPodNamespace, - imageRegistry: imageRegistry, + imageRegistry: c.imageRegistry, registryMirrors: c.registryMirrors, imagePullSecrets: c.imagePullSecrets, } } +func (c *containerImageResolver) WithImageRegistry(imageRegistry string) ContainerImageResolver { + clone := c.clone() + clone.imageRegistry = imageRegistry + return clone +} + func (c *containerImageResolver) WithRegistryMirrors(registryMirrors map[string]string) ContainerImageResolver { - return &containerImageResolver{ - logger: c.logger, - resolveForOpenShift: c.resolveForOpenShift, - imageCacher: c.imageCacher, - kubeClient: c.kubeClient, - operatorPodName: c.operatorPodName, - operatorPodNamespace: c.operatorPodNamespace, - imageRegistry: c.imageRegistry, - registryMirrors: registryMirrors, - imagePullSecrets: c.imagePullSecrets, - } + clone := c.clone() + clone.registryMirrors = registryMirrors + return clone } func (c *containerImageResolver) WithImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) ContainerImageResolver { - return &containerImageResolver{ - logger: c.logger, - resolveForOpenShift: c.resolveForOpenShift, - imageCacher: c.imageCacher, - kubeClient: c.kubeClient, - operatorPodName: c.operatorPodName, - operatorPodNamespace: c.operatorPodNamespace, - imageRegistry: c.imageRegistry, - registryMirrors: c.registryMirrors, - imagePullSecrets: imagePullSecrets, - } + clone := c.clone() + clone.imagePullSecrets = imagePullSecrets + return clone } func userImageOrDefault(defaultImage, defaultTag, userImage, userTag, userDigest string) string { diff --git a/pkg/utils/mondoo/container_image_resolver_test.go b/pkg/utils/mondoo/container_image_resolver_test.go index b55584718..45bbad854 100644 --- a/pkg/utils/mondoo/container_image_resolver_test.go +++ b/pkg/utils/mondoo/container_image_resolver_test.go @@ -248,3 +248,157 @@ func (s *ContainerImageResolverSuite) TestMondooOperatorImage_DigestWithTag() { func TestContainerImageResolverSuite(t *testing.T) { suite.Run(t, new(ContainerImageResolverSuite)) } + +func TestSplitImageParts(t *testing.T) { + tests := []struct { + name string + image string + expectedRegistry string + expectedRepoTag string + }{ + { + name: "ghcr.io image", + image: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + expectedRegistry: "ghcr.io", + expectedRepoTag: "mondoohq/mondoo-operator:v1.0.0", + }, + { + name: "docker.io image", + image: "docker.io/library/nginx:latest", + expectedRegistry: "docker.io", + expectedRepoTag: "library/nginx:latest", + }, + { + name: "quay.io image", + image: "quay.io/prometheus/prometheus:v2.40.0", + expectedRegistry: "quay.io", + expectedRepoTag: "prometheus/prometheus:v2.40.0", + }, + { + name: "private registry with port", + image: "registry.example.com:5000/myimage:tag", + expectedRegistry: "registry.example.com:5000", + expectedRepoTag: "myimage:tag", + }, + { + name: "localhost registry", + image: "localhost/myimage:tag", + expectedRegistry: "localhost", + expectedRepoTag: "myimage:tag", + }, + { + name: "localhost with port", + image: "localhost:5000/myimage:tag", + expectedRegistry: "localhost:5000", + expectedRepoTag: "myimage:tag", + }, + { + name: "image without registry (library image)", + image: "nginx:latest", + expectedRegistry: "", + expectedRepoTag: "nginx:latest", + }, + { + name: "image without registry (org/repo)", + image: "myorg/myimage:tag", + expectedRegistry: "", + expectedRepoTag: "myorg/myimage:tag", + }, + { + name: "image with digest", + image: "ghcr.io/mondoohq/cnspec@sha256:abc123", + expectedRegistry: "ghcr.io", + expectedRepoTag: "mondoohq/cnspec@sha256:abc123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parts := splitImageParts(tt.image) + if parts.registry != tt.expectedRegistry { + t.Errorf("registry: got %q, want %q", parts.registry, tt.expectedRegistry) + } + if parts.repositoryWithTag != tt.expectedRepoTag { + t.Errorf("repositoryWithTag: got %q, want %q", parts.repositoryWithTag, tt.expectedRepoTag) + } + }) + } +} + +func TestApplyImageRegistry(t *testing.T) { + tests := []struct { + name string + image string + imageRegistry string + registryMirrors map[string]string + expected string + }{ + { + name: "no registry configured", + image: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + imageRegistry: "", + expected: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + }, + { + name: "imageRegistry replaces registry", + image: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + imageRegistry: "artifactory.example.com/ghcr.io.docker", + expected: "artifactory.example.com/ghcr.io.docker/mondoohq/mondoo-operator:v1.0.0", + }, + { + name: "registryMirrors replaces specific registry", + image: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + registryMirrors: map[string]string{ + "ghcr.io": "artifactory.example.com/ghcr.io.docker", + }, + expected: "artifactory.example.com/ghcr.io.docker/mondoohq/mondoo-operator:v1.0.0", + }, + { + name: "registryMirrors takes precedence over imageRegistry", + image: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + registryMirrors: map[string]string{ + "ghcr.io": "mirror.example.com/ghcr", + }, + imageRegistry: "fallback.example.com", + expected: "mirror.example.com/ghcr/mondoohq/mondoo-operator:v1.0.0", + }, + { + name: "registryMirrors does not match - falls back to imageRegistry", + image: "quay.io/prometheus/prometheus:v2.40.0", + registryMirrors: map[string]string{ + "ghcr.io": "mirror.example.com/ghcr", + }, + imageRegistry: "fallback.example.com", + expected: "fallback.example.com/prometheus/prometheus:v2.40.0", + }, + { + name: "multiple registryMirrors", + image: "docker.io/library/nginx:latest", + registryMirrors: map[string]string{ + "ghcr.io": "mirror.example.com/ghcr", + "docker.io": "mirror.example.com/dockerhub", + "quay.io": "mirror.example.com/quay", + }, + expected: "mirror.example.com/dockerhub/library/nginx:latest", + }, + { + name: "imageRegistry with library image (no registry)", + image: "nginx:latest", + imageRegistry: "mirror.example.com", + expected: "mirror.example.com/nginx:latest", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resolver := &containerImageResolver{ + imageRegistry: tt.imageRegistry, + registryMirrors: tt.registryMirrors, + } + result := resolver.applyImageRegistry(tt.image) + if result != tt.expected { + t.Errorf("got %q, want %q", result, tt.expected) + } + }) + } +} diff --git a/pkg/utils/mondoo/fake/container_image_resolver.go b/pkg/utils/mondoo/fake/container_image_resolver.go index 8bfefde8a..ab714ceeb 100644 --- a/pkg/utils/mondoo/fake/container_image_resolver.go +++ b/pkg/utils/mondoo/fake/container_image_resolver.go @@ -79,3 +79,15 @@ func (c *noOpContainerImageResolver) WithRegistryMirrors(registryMirrors map[str func (c *noOpContainerImageResolver) WithImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) mondoo.ContainerImageResolver { return c } + +func (c *ContainerImageResolverMock) WithImageRegistry(imageRegistry string) mondoo.ContainerImageResolver { + return c +} + +func (c *ContainerImageResolverMock) WithRegistryMirrors(registryMirrors map[string]string) mondoo.ContainerImageResolver { + return c +} + +func (c *ContainerImageResolverMock) WithImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) mondoo.ContainerImageResolver { + return c +} diff --git a/pkg/utils/mondoo/integration.go b/pkg/utils/mondoo/integration.go index f5064e29d..eba5564dc 100644 --- a/pkg/utils/mondoo/integration.go +++ b/pkg/utils/mondoo/integration.go @@ -17,6 +17,7 @@ func IntegrationCheckIn( sa mondooclient.ServiceAccountCredentials, mondooClientBuilder MondooClientBuilder, httpProxy *string, + httpsProxy *string, noProxy *string, logger logr.Logger, ) error { @@ -29,6 +30,7 @@ func IntegrationCheckIn( ApiEndpoint: sa.ApiEndpoint, Token: token, HttpProxy: httpProxy, + HttpsProxy: httpsProxy, NoProxy: noProxy, }) if err != nil { diff --git a/pkg/utils/mondoo/token_exchange.go b/pkg/utils/mondoo/token_exchange.go index 24fbe048b..85d1d01d8 100644 --- a/pkg/utils/mondoo/token_exchange.go +++ b/pkg/utils/mondoo/token_exchange.go @@ -26,7 +26,7 @@ type MondooClientBuilder func(mondooclient.MondooClientOptions) (mondooclient.Mo // CreateServiceAccountFromToken will take the provided Mondoo token and exchange it with the Mondoo API // for a long lived Mondoo ServiceAccount -func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client, mondooClientBuilder MondooClientBuilder, withConsoleIntegration bool, serviceAccountSecret types.NamespacedName, tokenSecretData string, httpProxy *string, noProxy *string, log logr.Logger) error { +func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client, mondooClientBuilder MondooClientBuilder, withConsoleIntegration bool, serviceAccountSecret types.NamespacedName, tokenSecretData string, httpProxy *string, httpsProxy *string, noProxy *string, log logr.Logger) error { jwtString := strings.TrimSpace(tokenSecretData) parser := &jwt.Parser{} @@ -48,6 +48,7 @@ func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client ApiEndpoint: fmt.Sprintf("%v", apiEndpoint), Token: jwtString, HttpProxy: httpProxy, + HttpsProxy: httpsProxy, NoProxy: noProxy, } @@ -99,7 +100,7 @@ func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client // No easy way to retry this one-off CheckIn(). An error on initial CheckIn() // means we'll just retry on the regularly scheduled interval via the integration controller - _ = performInitialCheckIn(ctx, mondooClientBuilder, integrationMrn, *resp.Creds, httpProxy, noProxy, log) + _ = performInitialCheckIn(ctx, mondooClientBuilder, integrationMrn, *resp.Creds, httpProxy, httpsProxy, noProxy, log) } else { // Do a vanilla token-for-service-account exchange resp, err := mClient.ExchangeRegistrationToken(ctx, &mondooclient.ExchangeRegistrationTokenInput{ @@ -126,8 +127,8 @@ func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client return nil } -func performInitialCheckIn(ctx context.Context, mondooClientBuilder MondooClientBuilder, integrationMrn string, sa mondooclient.ServiceAccountCredentials, httpProxy *string, noProxy *string, logger logr.Logger) error { - if err := IntegrationCheckIn(ctx, integrationMrn, sa, mondooClientBuilder, httpProxy, noProxy, logger); err != nil { +func performInitialCheckIn(ctx context.Context, mondooClientBuilder MondooClientBuilder, integrationMrn string, sa mondooclient.ServiceAccountCredentials, httpProxy *string, httpsProxy *string, noProxy *string, logger logr.Logger) error { + if err := IntegrationCheckIn(ctx, integrationMrn, sa, mondooClientBuilder, httpProxy, httpsProxy, noProxy, logger); err != nil { logger.Error(err, "initial CheckIn() failed, will CheckIn() periodically", "integrationMRN", integrationMrn) return err } From c9fb18b2366e57196ef213fbb621099ea35d1874 Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Fri, 13 Feb 2026 19:37:05 +0100 Subject: [PATCH 3/9] fix: address review issues in MondooOperatorConfig integration - Fix imagePullSecrets to append instead of clobber existing secrets - Prefer HTTPS proxy for --api-proxy (Mondoo API is always HTTPS) - Add APIProxyURL helper to centralize proxy URL selection - Watch MondooOperatorConfig changes to trigger reconciliation - Share image cache across keychain changes (use pointer mutex) - Remove scaffolding comments from types - Fix "MondooOpertorConfig" typos in log messages Co-Authored-By: Claude Opus 4.6 --- api/v1alpha2/mondoooperatorconfig_types.go | 9 ------- .../operator/operator_status.go | 2 +- controllers/container_image/resources.go | 12 ++++++---- controllers/k8s_scan/resources.go | 24 ++++++++++++++----- controllers/mondooauditconfig_controller.go | 22 ++++++++++++++++- controllers/nodes/resources.go | 20 +++++++++++----- controllers/resource_watcher/resources.go | 14 ++++++++--- pkg/imagecache/imagecache.go | 8 ++++--- pkg/imagecache/imagecache_test.go | 6 +++-- pkg/utils/k8s/proxy.go | 9 +++++++ 10 files changed, 91 insertions(+), 35 deletions(-) diff --git a/api/v1alpha2/mondoooperatorconfig_types.go b/api/v1alpha2/mondoooperatorconfig_types.go index 74bbba5da..10df1583b 100644 --- a/api/v1alpha2/mondoooperatorconfig_types.go +++ b/api/v1alpha2/mondoooperatorconfig_types.go @@ -14,14 +14,8 @@ const ( MondooOperatorConfigName = "mondoo-operator-config" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // MondooOperatorConfigSpec defines the desired state of MondooOperatorConfig type MondooOperatorConfigSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - // Metrics controls the enabling/disabling of metrics report of mondoo-operator Metrics Metrics `json:"metrics,omitempty"` // Allows skipping Image resolution from upstream repository @@ -68,9 +62,6 @@ type Metrics struct { // MondooOperatorConfigStatus defines the observed state of MondooOperatorConfig type MondooOperatorConfigStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file - // Conditions includes more detailed status for the mondoo config // +optional Conditions []MondooOperatorConfigCondition `json:"conditions,omitempty"` diff --git a/cmd/mondoo-operator/operator/operator_status.go b/cmd/mondoo-operator/operator/operator_status.go index 78af7e594..08e4dfb8e 100644 --- a/cmd/mondoo-operator/operator/operator_status.go +++ b/cmd/mondoo-operator/operator/operator_status.go @@ -31,7 +31,7 @@ func checkForTerminatedState(ctx context.Context, nonCacheClient client.Client, if errors.IsNotFound(err) { logger.Info("MondooOperatorConfig not found, using defaults") } else { - logger.Error(err, "Failed to check for MondooOpertorConfig") + logger.Error(err, "Failed to check for MondooOperatorConfig") return err } } diff --git a/controllers/container_image/resources.go b/controllers/container_image/resources.go index d6a6fae84..41deece82 100644 --- a/controllers/container_image/resources.go +++ b/controllers/container_image/resources.go @@ -40,8 +40,10 @@ func CronJob(image, integrationMrn, clusterUid, privateRegistrySecretName string // Only add proxy settings if SkipProxyForCnspec is false // cnspec-based components may not properly handle NO_PROXY for internal domains - if !cfg.Spec.SkipProxyForCnspec && cfg.Spec.HttpProxy != nil { - cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) + if !cfg.Spec.SkipProxyForCnspec { + if apiProxy := k8s.APIProxyURL(cfg); apiProxy != nil { + cmd = append(cmd, "--api-proxy", *apiProxy) + } } envVars := feature_flags.AllFeatureFlagsAsEnv() @@ -160,9 +162,11 @@ func CronJob(image, integrationMrn, clusterUid, privateRegistrySecretName string // Add private registry secret if specified k8s.AddPrivateRegistryPullSecretToSpec(&cronjob.Spec.JobTemplate.Spec.Template.Spec, privateRegistrySecretName) - // Add imagePullSecrets from MondooOperatorConfig + // Append imagePullSecrets from MondooOperatorConfig (don't overwrite existing secrets) if len(cfg.Spec.ImagePullSecrets) > 0 { - cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = cfg.Spec.ImagePullSecrets + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = append( + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets, + cfg.Spec.ImagePullSecrets...) } return cronjob diff --git a/controllers/k8s_scan/resources.go b/controllers/k8s_scan/resources.go index 954ae8a26..f7bde9df0 100644 --- a/controllers/k8s_scan/resources.go +++ b/controllers/k8s_scan/resources.go @@ -60,8 +60,10 @@ func CronJob(image string, m *v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOpe } // Only add proxy if configured and not skipped for cnspec - if cfg.Spec.HttpProxy != nil && !cfg.Spec.SkipProxyForCnspec { - cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) + if !cfg.Spec.SkipProxyForCnspec { + if apiProxy := k8s.APIProxyURL(cfg); apiProxy != nil { + cmd = append(cmd, "--api-proxy", *apiProxy) + } } envVars := buildEnvVars(cfg) @@ -161,7 +163,6 @@ func CronJob(image string, m *v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOpe }, }, }, - ImagePullSecrets: cfg.Spec.ImagePullSecrets, }, }, }, @@ -171,6 +172,13 @@ func CronJob(image string, m *v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOpe }, } + // Add imagePullSecrets from MondooOperatorConfig + if len(cfg.Spec.ImagePullSecrets) > 0 { + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = append( + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets, + cfg.Spec.ImagePullSecrets...) + } + return cronjob } @@ -186,8 +194,10 @@ func ExternalClusterCronJob(image string, cluster v1alpha2.ExternalCluster, m *v } // Only add proxy if configured and not skipped for cnspec - if cfg.Spec.HttpProxy != nil && !cfg.Spec.SkipProxyForCnspec { - cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) + if !cfg.Spec.SkipProxyForCnspec { + if apiProxy := k8s.APIProxyURL(cfg); apiProxy != nil { + cmd = append(cmd, "--api-proxy", *apiProxy) + } } envVars := buildEnvVars(cfg) @@ -452,7 +462,9 @@ func ExternalClusterCronJob(image string, cluster v1alpha2.ExternalCluster, m *v // Add imagePullSecrets from MondooOperatorConfig if len(cfg.Spec.ImagePullSecrets) > 0 { - cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = cfg.Spec.ImagePullSecrets + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = append( + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets, + cfg.Spec.ImagePullSecrets...) } // Add private registry pull secrets if configured diff --git a/controllers/mondooauditconfig_controller.go b/controllers/mondooauditconfig_controller.go index 7043203c3..2c386601f 100644 --- a/controllers/mondooauditconfig_controller.go +++ b/controllers/mondooauditconfig_controller.go @@ -111,7 +111,7 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re if errors.IsNotFound(reconcileError) { log.Info("MondooOperatorConfig not found, using defaults") } else { - log.Error(reconcileError, "Failed to check for MondooOpertorConfig") + log.Error(reconcileError, "Failed to check for MondooOperatorConfig") return ctrl.Result{}, reconcileError } } @@ -507,6 +507,23 @@ func (r *MondooAuditConfigReconciler) exchangeTokenForServiceAccount(ctx context log) } +// operatorConfigRequestMapper maps MondooOperatorConfig changes to enqueue all MondooAuditConfigs +// for reconciliation, so proxy/registry changes take effect without waiting for the next scheduled reconcile. +func (r *MondooAuditConfigReconciler) operatorConfigRequestMapper(ctx context.Context, o client.Object) []reconcile.Request { + var requests []reconcile.Request + auditConfigs := &v1alpha2.MondooAuditConfigList{} + if err := r.List(ctx, auditConfigs); err != nil { + logger := ctrllog.Log.WithName("operator-config-watcher") + logger.Error(err, "Failed to list MondooAuditConfigs") + return requests + } + + for _, a := range auditConfigs.Items { + requests = append(requests, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&a)}) + } + return requests +} + // SetupWithManager sets up the controller with the Manager. func (r *MondooAuditConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). @@ -521,6 +538,9 @@ func (r *MondooAuditConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { &corev1.Node{}, handler.EnqueueRequestsFromMapFunc(r.nodeEventsRequestMapper), builder.WithPredicates(k8s.IgnoreGenericEventsPredicate{})). + Watches( + &v1alpha2.MondooOperatorConfig{}, + handler.EnqueueRequestsFromMapFunc(r.operatorConfigRequestMapper)). Complete(r) } diff --git a/controllers/nodes/resources.go b/controllers/nodes/resources.go index d381d5960..8d7e0e782 100644 --- a/controllers/nodes/resources.go +++ b/controllers/nodes/resources.go @@ -47,8 +47,10 @@ func CronJob(image string, node corev1.Node, m *v1alpha2.MondooAuditConfig, isOp } // Add API proxy if configured (respect SkipProxyForCnspec since node scanning uses cnspec) - if !cfg.Spec.SkipProxyForCnspec && cfg.Spec.HttpProxy != nil { - cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) + if !cfg.Spec.SkipProxyForCnspec { + if apiProxy := k8s.APIProxyURL(cfg); apiProxy != nil { + cmd = append(cmd, "--api-proxy", *apiProxy) + } } var proxyEnvVars []corev1.EnvVar @@ -175,7 +177,9 @@ func CronJob(image string, node corev1.Node, m *v1alpha2.MondooAuditConfig, isOp // Add imagePullSecrets from MondooOperatorConfig if len(cfg.Spec.ImagePullSecrets) > 0 { - cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = cfg.Spec.ImagePullSecrets + cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = append( + cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets, + cfg.Spec.ImagePullSecrets...) } return cj @@ -191,8 +195,10 @@ func DaemonSet(m v1alpha2.MondooAuditConfig, isOpenshift bool, image string, cfg "--timer", fmt.Sprintf("%d", m.Spec.Nodes.IntervalTimer), } // Add API proxy if configured (respect SkipProxyForCnspec since node scanning uses cnspec) - if !cfg.Spec.SkipProxyForCnspec && cfg.Spec.HttpProxy != nil { - cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) + if !cfg.Spec.SkipProxyForCnspec { + if apiProxy := k8s.APIProxyURL(cfg); apiProxy != nil { + cmd = append(cmd, "--api-proxy", *apiProxy) + } } var proxyEnvVars []corev1.EnvVar @@ -303,7 +309,9 @@ func DaemonSet(m v1alpha2.MondooAuditConfig, isOpenshift bool, image string, cfg // Add imagePullSecrets from MondooOperatorConfig if len(cfg.Spec.ImagePullSecrets) > 0 { - ds.Spec.Template.Spec.ImagePullSecrets = cfg.Spec.ImagePullSecrets + ds.Spec.Template.Spec.ImagePullSecrets = append( + ds.Spec.Template.Spec.ImagePullSecrets, + cfg.Spec.ImagePullSecrets...) } return ds diff --git a/controllers/resource_watcher/resources.go b/controllers/resource_watcher/resources.go index 9319902c4..618527f2e 100644 --- a/controllers/resource_watcher/resources.go +++ b/controllers/resource_watcher/resources.go @@ -92,8 +92,10 @@ func Deployment(image, integrationMRN, clusterUID string, m *v1alpha2.MondooAudi } // Add API proxy if configured (respect SkipProxyForCnspec since resource watcher uses cnspec) - if !cfg.Spec.SkipProxyForCnspec && cfg.Spec.HttpProxy != nil { - cmd = append(cmd, "--api-proxy", *cfg.Spec.HttpProxy) + if !cfg.Spec.SkipProxyForCnspec { + if apiProxy := k8s.APIProxyURL(cfg); apiProxy != nil { + cmd = append(cmd, "--api-proxy", *apiProxy) + } } // Add annotations (sorted for deterministic ordering) @@ -160,7 +162,6 @@ func Deployment(image, integrationMRN, clusterUID string, m *v1alpha2.MondooAudi TerminationMessagePolicy: corev1.TerminationMessageReadFile, }, }, - ImagePullSecrets: cfg.Spec.ImagePullSecrets, ServiceAccountName: m.Spec.Scanner.ServiceAccountName, Volumes: []corev1.Volume{ { @@ -194,5 +195,12 @@ func Deployment(image, integrationMRN, clusterUID string, m *v1alpha2.MondooAudi }, } + // Add imagePullSecrets from MondooOperatorConfig + if len(cfg.Spec.ImagePullSecrets) > 0 { + deployment.Spec.Template.Spec.ImagePullSecrets = append( + deployment.Spec.Template.Spec.ImagePullSecrets, + cfg.Spec.ImagePullSecrets...) + } + return deployment } diff --git a/pkg/imagecache/imagecache.go b/pkg/imagecache/imagecache.go index 380da5e95..4f2a68d33 100644 --- a/pkg/imagecache/imagecache.go +++ b/pkg/imagecache/imagecache.go @@ -34,7 +34,7 @@ type ImageCacher interface { type imageCache struct { images map[string]imageData - imagesMutex sync.RWMutex + imagesMutex *sync.RWMutex fetchImage func(string) (string, error) keychain authn.Keychain } @@ -121,7 +121,8 @@ func queryImageWithSHA(image string, keychain authn.Keychain) (string, error) { func NewImageCacher() ImageCacher { return &imageCache{ - images: map[string]imageData{}, + images: map[string]imageData{}, + imagesMutex: &sync.RWMutex{}, fetchImage: func(image string) (string, error) { return queryImageWithSHA(image, nil) }, @@ -130,7 +131,8 @@ func NewImageCacher() ImageCacher { func (i *imageCache) WithAuth(keychain authn.Keychain) ImageCacher { return &imageCache{ - images: map[string]imageData{}, + images: i.images, + imagesMutex: i.imagesMutex, fetchImage: func(image string) (string, error) { return queryImageWithSHA(image, keychain) }, diff --git a/pkg/imagecache/imagecache_test.go b/pkg/imagecache/imagecache_test.go index 2b517d41f..691ba8247 100644 --- a/pkg/imagecache/imagecache_test.go +++ b/pkg/imagecache/imagecache_test.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "sync" "testing" "time" @@ -82,8 +83,9 @@ func TestCache(t *testing.T) { t.Run(test.name, func(t *testing.T) { // Arrange testCache := &imageCache{ - images: test.imagesMap, - fetchImage: test.fetchImageFunc, + images: test.imagesMap, + imagesMutex: &sync.RWMutex{}, + fetchImage: test.fetchImageFunc, } // Act diff --git a/pkg/utils/k8s/proxy.go b/pkg/utils/k8s/proxy.go index 49a48fa84..40d5212ae 100644 --- a/pkg/utils/k8s/proxy.go +++ b/pkg/utils/k8s/proxy.go @@ -35,3 +35,12 @@ func ProxyEnvVars(cfg v1alpha2.MondooOperatorConfig) []corev1.EnvVar { return envVars } + +// APIProxyURL returns the proxy URL to use for Mondoo API calls. +// Since the Mondoo API is always HTTPS, it prefers HttpsProxy over HttpProxy. +func APIProxyURL(cfg v1alpha2.MondooOperatorConfig) *string { + if cfg.Spec.HttpsProxy != nil { + return cfg.Spec.HttpsProxy + } + return cfg.Spec.HttpProxy +} From a36a4f34c3ed76345a76731afea363ce913675db Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Fri, 13 Feb 2026 19:41:35 +0100 Subject: [PATCH 4/9] test: add proxy and registry integration tests for resource builders Add comprehensive test coverage for MondooOperatorConfig proxy and registry mirror integration across all resource builders: - pkg/utils/k8s: ProxyEnvVars and APIProxyURL unit tests - k8s_scan: proxy, skip-proxy, imagePullSecrets, container-proxy tests - container_image: proxy, skip-proxy, imagePullSecrets, container-proxy tests - nodes: CronJob and DaemonSet proxy/skip-proxy/imagePullSecrets tests - resource_watcher: HTTPS preference, skip-proxy, env vars, imagePullSecrets tests Co-Authored-By: Claude Opus 4.6 --- controllers/container_image/resources_test.go | 162 ++++++++++++ controllers/k8s_scan/resources_test.go | 235 ++++++++++++++++++ controllers/nodes/resources_test.go | 128 ++++++++++ .../resource_watcher/resources_test.go | 147 +++++++++++ pkg/utils/k8s/proxy_test.go | 90 +++++++ 5 files changed, 762 insertions(+) create mode 100644 pkg/utils/k8s/proxy_test.go diff --git a/controllers/container_image/resources_test.go b/controllers/container_image/resources_test.go index 825c42226..13e7efffa 100644 --- a/controllers/container_image/resources_test.go +++ b/controllers/container_image/resources_test.go @@ -4,12 +4,15 @@ package container_image import ( + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory" "go.mondoo.com/mondoo-operator/api/v1alpha2" @@ -17,6 +20,20 @@ import ( const testClusterUID = "abcdefg" +func testAuditConfig() *v1alpha2.MondooAuditConfig { + return &v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mondoo-client", + Namespace: "mondoo-operator", + }, + Spec: v1alpha2.MondooAuditConfigSpec{ + Containers: v1alpha2.Containers{ + Schedule: "0 * * * *", + }, + }, + } +} + func TestInventory_WithAnnotations(t *testing.T) { auditConfig := v1alpha2.MondooAuditConfig{ ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, @@ -40,3 +57,148 @@ func TestInventory_WithAnnotations(t *testing.T) { assert.Equal(t, "platform", asset.Annotations["team"], "asset %s missing team annotation", asset.Name) } } + +func TestCronJob_WithProxy(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + cj := CronJob("test-image:latest", "", testClusterUID, "", m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy") + assert.Contains(t, cmdStr, "https://proxy:8443") + + envMap := envToMap(container.Env) + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) +} + +func TestCronJob_SkipProxyForCnspec(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + SkipProxyForCnspec: true, + }, + } + + cj := CronJob("test-image:latest", "", testClusterUID, "", m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.NotContains(t, cmdStr, "--api-proxy") + + envMap := envToMap(container.Env) + _, hasHTTPProxy := envMap["HTTP_PROXY"] + _, hasHTTPSProxy := envMap["HTTPS_PROXY"] + assert.False(t, hasHTTPProxy, "HTTP_PROXY should not be set when SkipProxyForCnspec is true") + assert.False(t, hasHTTPSProxy, "HTTPS_PROXY should not be set when SkipProxyForCnspec is true") +} + +func TestCronJob_WithImagePullSecrets(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "my-registry-secret"}, + }, + }, + } + + cj := CronJob("test-image:latest", "", testClusterUID, "", m, cfg) + secrets := cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets + require.Len(t, secrets, 1) + assert.Equal(t, "my-registry-secret", secrets[0].Name) +} + +func TestCronJob_ImagePullSecrets_AppendsMultiple(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "secret-one"}, + {Name: "secret-two"}, + }, + }, + } + + cj := CronJob("test-image:latest", "", testClusterUID, "", m, cfg) + secrets := cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets + + require.Len(t, secrets, 2) + assert.Equal(t, "secret-one", secrets[0].Name) + assert.Equal(t, "secret-two", secrets[1].Name) +} + +func TestCronJob_PrivateRegistrySecretMountsDockerConfig(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{} + + cj := CronJob("test-image:latest", "", testClusterUID, "private-registry-secret", m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + // Private registry secret should be mounted as a Docker config volume, not as ImagePullSecrets + envMap := envToMap(container.Env) + assert.Equal(t, "/etc/opt/mondoo/docker", envMap["DOCKER_CONFIG"]) + + found := false + for _, vm := range container.VolumeMounts { + if vm.Name == "pull-secrets" { + found = true + assert.Equal(t, "/etc/opt/mondoo/docker", vm.MountPath) + } + } + assert.True(t, found, "pull-secrets volume mount should be present") +} + +func TestInventory_WithContainerProxy(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + } + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ContainerProxy: ptr.To("http://container-proxy:3128"), + }, + } + + invStr, err := Inventory("", testClusterUID, auditConfig, cfg) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + assert.Equal(t, "http://container-proxy:3128", inv.Spec.Assets[0].Connections[0].Options["container-proxy"]) +} + +func TestInventory_WithoutContainerProxy(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + } + + invStr, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{}) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + _, hasContainerProxy := inv.Spec.Assets[0].Connections[0].Options["container-proxy"] + assert.False(t, hasContainerProxy) +} + +// envToMap converts a slice of EnvVar to a map for easy lookup. +func envToMap(envVars []corev1.EnvVar) map[string]string { + m := make(map[string]string, len(envVars)) + for _, e := range envVars { + m[e.Name] = e.Value + } + return m +} diff --git a/controllers/k8s_scan/resources_test.go b/controllers/k8s_scan/resources_test.go index 63ad697e7..3f37c7b7e 100644 --- a/controllers/k8s_scan/resources_test.go +++ b/controllers/k8s_scan/resources_test.go @@ -4,12 +4,15 @@ package k8s_scan import ( + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory" "go.mondoo.com/mondoo-operator/api/v1alpha2" @@ -17,6 +20,20 @@ import ( const testClusterUID = "abcdefg" +func testAuditConfig() *v1alpha2.MondooAuditConfig { + return &v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mondoo-client", + Namespace: "mondoo-operator", + }, + Spec: v1alpha2.MondooAuditConfigSpec{ + KubernetesResources: v1alpha2.KubernetesResources{ + Schedule: "0 * * * *", + }, + }, + } +} + func TestInventory_WithAnnotations(t *testing.T) { auditConfig := v1alpha2.MondooAuditConfig{ ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, @@ -68,3 +85,221 @@ func TestExternalClusterInventory_WithAnnotations(t *testing.T) { assert.Equal(t, "security", asset.Annotations["team"], "asset %s missing team annotation", asset.Name) } } + +func TestCronJob_WithProxy(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + cj := CronJob("test-image:latest", m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy") + assert.Contains(t, cmdStr, "https://proxy:8443") + + envMap := envToMap(container.Env) + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) +} + +func TestCronJob_HttpsProxyPreferred(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + cj := CronJob("test-image:latest", m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy https://proxy:8443") + assert.NotContains(t, cmdStr, "http://proxy:8080") +} + +func TestCronJob_SkipProxyForCnspec(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + SkipProxyForCnspec: true, + }, + } + + cj := CronJob("test-image:latest", m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.NotContains(t, cmdStr, "--api-proxy") + + envMap := envToMap(container.Env) + _, hasHTTPProxy := envMap["HTTP_PROXY"] + _, hasHTTPSProxy := envMap["HTTPS_PROXY"] + assert.False(t, hasHTTPProxy, "HTTP_PROXY should not be set when SkipProxyForCnspec is true") + assert.False(t, hasHTTPSProxy, "HTTPS_PROXY should not be set when SkipProxyForCnspec is true") +} + +func TestCronJob_WithImagePullSecrets(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "my-registry-secret"}, + }, + }, + } + + cj := CronJob("test-image:latest", m, cfg) + secrets := cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets + require.Len(t, secrets, 1) + assert.Equal(t, "my-registry-secret", secrets[0].Name) +} + +func TestExternalClusterCronJob_WithProxy(t *testing.T) { + m := testAuditConfig() + cluster := v1alpha2.ExternalCluster{ + Name: "remote", + KubeconfigSecretRef: &corev1.LocalObjectReference{ + Name: "kubeconfig-secret", + }, + } + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + cj := ExternalClusterCronJob("test-image:latest", cluster, m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy") + assert.Contains(t, cmdStr, "https://proxy:8443") + + envMap := envToMap(container.Env) + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) +} + +func TestExternalClusterCronJob_SkipProxy(t *testing.T) { + m := testAuditConfig() + cluster := v1alpha2.ExternalCluster{ + Name: "remote", + KubeconfigSecretRef: &corev1.LocalObjectReference{ + Name: "kubeconfig-secret", + }, + } + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + SkipProxyForCnspec: true, + }, + } + + cj := ExternalClusterCronJob("test-image:latest", cluster, m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.NotContains(t, cmdStr, "--api-proxy") + + envMap := envToMap(container.Env) + _, hasHTTPProxy := envMap["HTTP_PROXY"] + assert.False(t, hasHTTPProxy) +} + +func TestExternalClusterCronJob_ImagePullSecrets(t *testing.T) { + m := testAuditConfig() + cluster := v1alpha2.ExternalCluster{ + Name: "remote", + KubeconfigSecretRef: &corev1.LocalObjectReference{ + Name: "kubeconfig-secret", + }, + } + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "my-registry-secret"}, + }, + }, + } + + cj := ExternalClusterCronJob("test-image:latest", cluster, m, cfg) + secrets := cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets + require.Len(t, secrets, 1) + assert.Equal(t, "my-registry-secret", secrets[0].Name) +} + +func TestInventory_WithContainerProxy(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + } + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ContainerProxy: ptr.To("http://container-proxy:3128"), + }, + } + + invStr, err := Inventory("", testClusterUID, auditConfig, cfg) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + assert.Equal(t, "http://container-proxy:3128", inv.Spec.Assets[0].Connections[0].Options["container-proxy"]) +} + +func TestInventory_WithoutContainerProxy(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + } + + invStr, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{}) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + _, hasContainerProxy := inv.Spec.Assets[0].Connections[0].Options["container-proxy"] + assert.False(t, hasContainerProxy) +} + +func TestExternalClusterInventory_WithContainerProxy(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + } + cluster := v1alpha2.ExternalCluster{Name: "remote-cluster"} + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ContainerProxy: ptr.To("http://container-proxy:3128"), + }, + } + + invStr, err := ExternalClusterInventory("", testClusterUID, cluster, auditConfig, cfg) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + assert.Equal(t, "http://container-proxy:3128", inv.Spec.Assets[0].Connections[0].Options["container-proxy"]) +} + +// envToMap converts a slice of EnvVar to a map for easy lookup. +func envToMap(envVars []corev1.EnvVar) map[string]string { + m := make(map[string]string, len(envVars)) + for _, e := range envVars { + m[e.Name] = e.Value + } + return m +} diff --git a/controllers/nodes/resources_test.go b/controllers/nodes/resources_test.go index b2953a6bc..f11f7bf97 100644 --- a/controllers/nodes/resources_test.go +++ b/controllers/nodes/resources_test.go @@ -6,6 +6,7 @@ package nodes import ( "crypto/sha256" "fmt" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -20,6 +21,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) const ( @@ -263,6 +265,132 @@ func TestInventory_WithAnnotations(t *testing.T) { } } +func TestCronJob_WithProxy(t *testing.T) { + testNode := corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "test-node-name"}} + mac := testMondooAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + cj := CronJob("test123", testNode, mac, false, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy") + assert.Contains(t, cmdStr, "https://proxy:8443") + + envMap := envToMap(container.Env) + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) +} + +func TestCronJob_SkipProxyForCnspec(t *testing.T) { + testNode := corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "test-node-name"}} + mac := testMondooAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + SkipProxyForCnspec: true, + }, + } + + cj := CronJob("test123", testNode, mac, false, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.NotContains(t, cmdStr, "--api-proxy") + + envMap := envToMap(container.Env) + _, hasHTTPProxy := envMap["HTTP_PROXY"] + assert.False(t, hasHTTPProxy, "HTTP_PROXY should not be set when SkipProxyForCnspec is true") +} + +func TestCronJob_WithImagePullSecrets(t *testing.T) { + testNode := corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "test-node-name"}} + mac := testMondooAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "my-registry-secret"}, + }, + }, + } + + cj := CronJob("test123", testNode, mac, false, cfg) + secrets := cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets + require.Len(t, secrets, 1) + assert.Equal(t, "my-registry-secret", secrets[0].Name) +} + +func TestDaemonSet_WithProxy(t *testing.T) { + mac := *testMondooAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + ds := DaemonSet(mac, false, "test123", cfg, nil) + container := ds.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy") + assert.Contains(t, cmdStr, "https://proxy:8443") + + envMap := envToMap(container.Env) + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) +} + +func TestDaemonSet_SkipProxyForCnspec(t *testing.T) { + mac := *testMondooAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + SkipProxyForCnspec: true, + }, + } + + ds := DaemonSet(mac, false, "test123", cfg, nil) + container := ds.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.NotContains(t, cmdStr, "--api-proxy") + + envMap := envToMap(container.Env) + _, hasHTTPProxy := envMap["HTTP_PROXY"] + assert.False(t, hasHTTPProxy, "HTTP_PROXY should not be set when SkipProxyForCnspec is true") +} + +func TestDaemonSet_WithImagePullSecrets(t *testing.T) { + mac := *testMondooAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "my-registry-secret"}, + }, + }, + } + + ds := DaemonSet(mac, false, "test123", cfg, nil) + secrets := ds.Spec.Template.Spec.ImagePullSecrets + require.Len(t, secrets, 1) + assert.Equal(t, "my-registry-secret", secrets[0].Name) +} + +// envToMap converts a slice of EnvVar to a map for easy lookup. +func envToMap(envVars []corev1.EnvVar) map[string]string { + m := make(map[string]string, len(envVars)) + for _, e := range envVars { + m[e.Name] = e.Value + } + return m +} + func testMondooAuditConfig() *v1alpha2.MondooAuditConfig { return &v1alpha2.MondooAuditConfig{ ObjectMeta: metav1.ObjectMeta{ diff --git a/controllers/resource_watcher/resources_test.go b/controllers/resource_watcher/resources_test.go index c3cc95679..d0aee3275 100644 --- a/controllers/resource_watcher/resources_test.go +++ b/controllers/resource_watcher/resources_test.go @@ -4,11 +4,15 @@ package resource_watcher import ( + "strings" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "go.mondoo.com/mondoo-operator/api/v1alpha2" ) @@ -362,3 +366,146 @@ func TestDeployment_EmptyClusterUIDAndIntegrationMRN(t *testing.T) { assert.NotContains(t, cmdStr, "--cluster-uid") assert.NotContains(t, cmdStr, "--integration-mrn") } + +func TestDeployment_HttpsProxyPreferred(t *testing.T) { + config := &v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-config", + Namespace: "mondoo-operator", + }, + Spec: v1alpha2.MondooAuditConfigSpec{ + KubernetesResources: v1alpha2.KubernetesResources{ + Enable: true, + ResourceWatcher: v1alpha2.ResourceWatcherSpec{ + Enable: true, + }, + }, + }, + } + + operatorConfig := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + deployment := Deployment("ghcr.io/mondoohq/cnspec:latest", "", "", config, operatorConfig) + container := deployment.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy https://proxy:8443") + assert.NotContains(t, cmdStr, "http://proxy:8080") +} + +func TestDeployment_SkipProxyForCnspec(t *testing.T) { + config := &v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-config", + Namespace: "mondoo-operator", + }, + Spec: v1alpha2.MondooAuditConfigSpec{ + KubernetesResources: v1alpha2.KubernetesResources{ + Enable: true, + ResourceWatcher: v1alpha2.ResourceWatcherSpec{ + Enable: true, + }, + }, + }, + } + + operatorConfig := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + SkipProxyForCnspec: true, + }, + } + + deployment := Deployment("ghcr.io/mondoohq/cnspec:latest", "", "", config, operatorConfig) + container := deployment.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.NotContains(t, cmdStr, "--api-proxy") + + envMap := envToMap(container.Env) + _, hasHTTPProxy := envMap["HTTP_PROXY"] + _, hasHTTPSProxy := envMap["HTTPS_PROXY"] + assert.False(t, hasHTTPProxy, "HTTP_PROXY should not be set when SkipProxyForCnspec is true") + assert.False(t, hasHTTPSProxy, "HTTPS_PROXY should not be set when SkipProxyForCnspec is true") +} + +func TestDeployment_ProxyEnvVars(t *testing.T) { + config := &v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-config", + Namespace: "mondoo-operator", + }, + Spec: v1alpha2.MondooAuditConfigSpec{ + KubernetesResources: v1alpha2.KubernetesResources{ + Enable: true, + ResourceWatcher: v1alpha2.ResourceWatcherSpec{ + Enable: true, + }, + }, + }, + } + + operatorConfig := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + NoProxy: ptr.To("localhost,10.0.0.0/8"), + }, + } + + deployment := Deployment("ghcr.io/mondoohq/cnspec:latest", "", "", config, operatorConfig) + container := deployment.Spec.Template.Spec.Containers[0] + + envMap := envToMap(container.Env) + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "http://proxy:8080", envMap["http_proxy"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["https_proxy"]) + assert.Equal(t, "localhost,10.0.0.0/8", envMap["NO_PROXY"]) + assert.Equal(t, "localhost,10.0.0.0/8", envMap["no_proxy"]) +} + +func TestDeployment_WithImagePullSecrets(t *testing.T) { + config := &v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-config", + Namespace: "mondoo-operator", + }, + Spec: v1alpha2.MondooAuditConfigSpec{ + KubernetesResources: v1alpha2.KubernetesResources{ + Enable: true, + ResourceWatcher: v1alpha2.ResourceWatcherSpec{ + Enable: true, + }, + }, + }, + } + + operatorConfig := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "my-registry-secret"}, + }, + }, + } + + deployment := Deployment("ghcr.io/mondoohq/cnspec:latest", "", "", config, operatorConfig) + secrets := deployment.Spec.Template.Spec.ImagePullSecrets + require.Len(t, secrets, 1) + assert.Equal(t, "my-registry-secret", secrets[0].Name) +} + +// envToMap converts a slice of EnvVar to a map for easy lookup. +func envToMap(envVars []corev1.EnvVar) map[string]string { + m := make(map[string]string, len(envVars)) + for _, e := range envVars { + m[e.Name] = e.Value + } + return m +} diff --git a/pkg/utils/k8s/proxy_test.go b/pkg/utils/k8s/proxy_test.go new file mode 100644 index 000000000..4d760fc84 --- /dev/null +++ b/pkg/utils/k8s/proxy_test.go @@ -0,0 +1,90 @@ +// Copyright Mondoo, Inc. 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package k8s + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.mondoo.com/mondoo-operator/api/v1alpha2" + "k8s.io/utils/ptr" +) + +func TestProxyEnvVars_AllSet(t *testing.T) { + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + NoProxy: ptr.To("localhost,10.0.0.0/8"), + }, + } + + envVars := ProxyEnvVars(cfg) + require.Len(t, envVars, 6) + + envMap := map[string]string{} + for _, e := range envVars { + envMap[e.Name] = e.Value + } + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "http://proxy:8080", envMap["http_proxy"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["https_proxy"]) + assert.Equal(t, "localhost,10.0.0.0/8", envMap["NO_PROXY"]) + assert.Equal(t, "localhost,10.0.0.0/8", envMap["no_proxy"]) +} + +func TestProxyEnvVars_OnlyHttpProxy(t *testing.T) { + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + }, + } + + envVars := ProxyEnvVars(cfg) + require.Len(t, envVars, 2) + assert.Equal(t, "HTTP_PROXY", envVars[0].Name) + assert.Equal(t, "http_proxy", envVars[1].Name) +} + +func TestProxyEnvVars_NoneSet(t *testing.T) { + cfg := v1alpha2.MondooOperatorConfig{} + + envVars := ProxyEnvVars(cfg) + assert.Empty(t, envVars) +} + +func TestAPIProxyURL_PrefersHttpsProxy(t *testing.T) { + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + result := APIProxyURL(cfg) + require.NotNil(t, result) + assert.Equal(t, "https://proxy:8443", *result) +} + +func TestAPIProxyURL_FallsBackToHttpProxy(t *testing.T) { + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + }, + } + + result := APIProxyURL(cfg) + require.NotNil(t, result) + assert.Equal(t, "http://proxy:8080", *result) +} + +func TestAPIProxyURL_NilWhenNoneSet(t *testing.T) { + cfg := v1alpha2.MondooOperatorConfig{} + + result := APIProxyURL(cfg) + assert.Nil(t, result) +} From 29ca6c9427c41a803e8bf600d8148cdb5a9e8ffa Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Fri, 13 Feb 2026 20:14:00 +0100 Subject: [PATCH 5/9] fix: increase CronJob schedule buffer in integration tests The cron schedule only uses the minute field, so the effective buffer between function call time and the CronJob trigger is (targetMinuteStart - now), which could be as low as 16 seconds with the old 75-second offset. This wasn't enough when leader election takes ~46 seconds, causing the CronJob to miss its scheduled minute and wait an hour for the next trigger. Increase the offset from 1m15s to 2m30s, guaranteeing at least ~91 seconds of buffer regardless of when in the current minute the function is called. Co-Authored-By: Claude Opus 4.6 --- tests/framework/utils/audit_config.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/framework/utils/audit_config.go b/tests/framework/utils/audit_config.go index 75c7f32a2..fa3d15b94 100644 --- a/tests/framework/utils/audit_config.go +++ b/tests/framework/utils/audit_config.go @@ -37,7 +37,10 @@ func init() { // DefaultAuditConfig instead. func DefaultAuditConfigMinimal(ns string, workloads, containers, nodes bool) mondoov2.MondooAuditConfig { now := time.Now() - startScan := now.Add(time.Minute).Add(time.Second * 15) + // The cron schedule only uses the minute field, so the real buffer is + // (targetMinuteStart - now). With 2m30s offset the minimum buffer is ~91s, + // which safely covers leader election (~45s) plus CronJob creation time. + startScan := now.Add(2 * time.Minute).Add(30 * time.Second) schedule := fmt.Sprintf("%d * * * *", startScan.Minute()) auditConfig := mondoov2.MondooAuditConfig{ TypeMeta: v1.TypeMeta{ From 3ff352cacf2611126ae5946cc101b8d8fe1c7fa9 Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Fri, 13 Feb 2026 23:30:21 +0100 Subject: [PATCH 6/9] fix: include CRDs in helm template test output The CRDs live in charts/mondoo-operator/crds/ (not templates/), so helm template doesn't render them by default. Add --include-crds to the Template helper so TestHelmTemplate can verify CRDs are present. Co-Authored-By: Claude Opus 4.6 --- tests/framework/utils/helm_helper.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/framework/utils/helm_helper.go b/tests/framework/utils/helm_helper.go index 799305591..d1c073a01 100644 --- a/tests/framework/utils/helm_helper.go +++ b/tests/framework/utils/helm_helper.go @@ -84,6 +84,7 @@ func (h *HelmHelper) Template(releaseName, chartPath, namespace string, values m releaseName, chartPath, "--namespace", namespace, + "--include-crds", } for k, v := range values { From 18033497b628d0828a16dc9c551375549e864d02 Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Fri, 13 Feb 2026 23:48:10 +0100 Subject: [PATCH 7/9] fix: correct CronJob schedule buffer and increase retry window The 2m30s buffer was too aggressive - worst-case trigger time (~150s) exceeded the retry window (100s), causing tests to time out before CronJobs fired. Reduce buffer to 2m (61-120s range) and double RetryLoop from 50 to 100 (200s window) to accommodate the buffer. Co-Authored-By: Claude Opus 4.6 --- tests/framework/utils/audit_config.go | 7 ++++--- tests/framework/utils/k8s_helper.go | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/framework/utils/audit_config.go b/tests/framework/utils/audit_config.go index fa3d15b94..4e1720cbf 100644 --- a/tests/framework/utils/audit_config.go +++ b/tests/framework/utils/audit_config.go @@ -38,9 +38,10 @@ func init() { func DefaultAuditConfigMinimal(ns string, workloads, containers, nodes bool) mondoov2.MondooAuditConfig { now := time.Now() // The cron schedule only uses the minute field, so the real buffer is - // (targetMinuteStart - now). With 2m30s offset the minimum buffer is ~91s, - // which safely covers leader election (~45s) plus CronJob creation time. - startScan := now.Add(2 * time.Minute).Add(30 * time.Second) + // (targetMinuteStart - now). With a 2m offset the minimum buffer is ~61s, + // which safely covers leader election (~45s) plus CronJob creation time, + // while keeping the worst-case trigger (~120s) within the retry window. + startScan := now.Add(2 * time.Minute) schedule := fmt.Sprintf("%d * * * *", startScan.Minute()) auditConfig := mondoov2.MondooAuditConfig{ TypeMeta: v1.TypeMeta{ diff --git a/tests/framework/utils/k8s_helper.go b/tests/framework/utils/k8s_helper.go index 044b4b595..7a97926c3 100644 --- a/tests/framework/utils/k8s_helper.go +++ b/tests/framework/utils/k8s_helper.go @@ -43,7 +43,7 @@ import ( const ( cmd = "kubectl" RetryInterval = 2 - RetryLoop = 50 + RetryLoop = 100 SkipVersionCheckEnvVar = "SKIP_VERSION_CHECK" ) From f33245450d5cf103e1fb60554ad016d1f7ac1bc7 Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Sat, 14 Feb 2026 17:14:31 +0100 Subject: [PATCH 8/9] fix: improve integration test reliability and cleanup - Add CronJobRetryLoop (600s) for WaitUntilCronJobsSuccessful to handle variable scan durations without affecting other retry timeouts - Clean up stale k3d target cluster before creating in external cluster tests - Add --ignore-not-found to pod deletion in AfterTest cleanup - Downgrade completed CronJob pod describe failure from ERROR to WARN - Regenerate CRD and RBAC manifests for updated type docs and job delete verb - Fix whitespace alignment in container_image_resolver_test.go Co-Authored-By: Claude Opus 4.6 --- .../k8s.mondoo.com_mondoooperatorconfigs.yaml | 10 ++- config/rbac/role.yaml | 1 + .../mondoo/container_image_resolver_test.go | 80 +++++++++---------- tests/framework/utils/k8s_helper.go | 28 ++++--- tests/integration/audit_config_base_suite.go | 2 +- tests/integration/external_cluster_test.go | 3 + 6 files changed, 67 insertions(+), 57 deletions(-) diff --git a/config/crd/bases/k8s.mondoo.com_mondoooperatorconfigs.yaml b/config/crd/bases/k8s.mondoo.com_mondoooperatorconfigs.yaml index 3121a0c37..49622c75a 100644 --- a/config/crd/bases/k8s.mondoo.com_mondoooperatorconfigs.yaml +++ b/config/crd/bases/k8s.mondoo.com_mondoooperatorconfigs.yaml @@ -75,10 +75,10 @@ spec: type: array imageRegistry: description: |- - ImageRegistry specifies a custom container image registry to use for all Mondoo images. - This allows using a private registry mirror (e.g., artifactory.example.com/ghcr.io.docker). - If set, all image references will be prefixed with this registry. - Deprecated: Use RegistryMirrors for more flexible registry mapping. + ImageRegistry specifies a custom container image registry prefix for all Mondoo images. + Use this for simple registry mirrors where all images go to the same mirror. + Example: "artifactory.example.com/ghcr.io.docker" + For more complex setups with multiple source registries, use RegistryMirrors instead. type: string metrics: description: Metrics controls the enabling/disabling of metrics report @@ -103,12 +103,14 @@ spec: type: string description: |- RegistryMirrors specifies a mapping of public registries to private mirrors. + Use this when you need to map different source registries to different mirrors. The key is the public registry (e.g., "ghcr.io", "docker.io", "quay.io") and the value is the private mirror (e.g., "artifactory.example.com/ghcr.io.docker"). Example: registryMirrors: ghcr.io: artifactory.example.com/ghcr.io.docker docker.io: artifactory.example.com/hub.docker.com + Note: If both ImageRegistry and RegistryMirrors are set, RegistryMirrors takes precedence. type: object skipContainerResolution: description: Allows skipping Image resolution from upstream repository diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index dd5d79afe..a264b568c 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -85,6 +85,7 @@ rules: resources: - jobs verbs: + - delete - deletecollection - get - list diff --git a/pkg/utils/mondoo/container_image_resolver_test.go b/pkg/utils/mondoo/container_image_resolver_test.go index 45bbad854..88553f38a 100644 --- a/pkg/utils/mondoo/container_image_resolver_test.go +++ b/pkg/utils/mondoo/container_image_resolver_test.go @@ -251,64 +251,64 @@ func TestContainerImageResolverSuite(t *testing.T) { func TestSplitImageParts(t *testing.T) { tests := []struct { - name string - image string - expectedRegistry string - expectedRepoTag string + name string + image string + expectedRegistry string + expectedRepoTag string }{ { - name: "ghcr.io image", - image: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", - expectedRegistry: "ghcr.io", - expectedRepoTag: "mondoohq/mondoo-operator:v1.0.0", + name: "ghcr.io image", + image: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + expectedRegistry: "ghcr.io", + expectedRepoTag: "mondoohq/mondoo-operator:v1.0.0", }, { - name: "docker.io image", - image: "docker.io/library/nginx:latest", - expectedRegistry: "docker.io", - expectedRepoTag: "library/nginx:latest", + name: "docker.io image", + image: "docker.io/library/nginx:latest", + expectedRegistry: "docker.io", + expectedRepoTag: "library/nginx:latest", }, { - name: "quay.io image", - image: "quay.io/prometheus/prometheus:v2.40.0", - expectedRegistry: "quay.io", - expectedRepoTag: "prometheus/prometheus:v2.40.0", + name: "quay.io image", + image: "quay.io/prometheus/prometheus:v2.40.0", + expectedRegistry: "quay.io", + expectedRepoTag: "prometheus/prometheus:v2.40.0", }, { - name: "private registry with port", - image: "registry.example.com:5000/myimage:tag", - expectedRegistry: "registry.example.com:5000", - expectedRepoTag: "myimage:tag", + name: "private registry with port", + image: "registry.example.com:5000/myimage:tag", + expectedRegistry: "registry.example.com:5000", + expectedRepoTag: "myimage:tag", }, { - name: "localhost registry", - image: "localhost/myimage:tag", - expectedRegistry: "localhost", - expectedRepoTag: "myimage:tag", + name: "localhost registry", + image: "localhost/myimage:tag", + expectedRegistry: "localhost", + expectedRepoTag: "myimage:tag", }, { - name: "localhost with port", - image: "localhost:5000/myimage:tag", - expectedRegistry: "localhost:5000", - expectedRepoTag: "myimage:tag", + name: "localhost with port", + image: "localhost:5000/myimage:tag", + expectedRegistry: "localhost:5000", + expectedRepoTag: "myimage:tag", }, { - name: "image without registry (library image)", - image: "nginx:latest", - expectedRegistry: "", - expectedRepoTag: "nginx:latest", + name: "image without registry (library image)", + image: "nginx:latest", + expectedRegistry: "", + expectedRepoTag: "nginx:latest", }, { - name: "image without registry (org/repo)", - image: "myorg/myimage:tag", - expectedRegistry: "", - expectedRepoTag: "myorg/myimage:tag", + name: "image without registry (org/repo)", + image: "myorg/myimage:tag", + expectedRegistry: "", + expectedRepoTag: "myorg/myimage:tag", }, { - name: "image with digest", - image: "ghcr.io/mondoohq/cnspec@sha256:abc123", - expectedRegistry: "ghcr.io", - expectedRepoTag: "mondoohq/cnspec@sha256:abc123", + name: "image with digest", + image: "ghcr.io/mondoohq/cnspec@sha256:abc123", + expectedRegistry: "ghcr.io", + expectedRepoTag: "mondoohq/cnspec@sha256:abc123", }, } diff --git a/tests/framework/utils/k8s_helper.go b/tests/framework/utils/k8s_helper.go index 7a97926c3..0ab0cf6a4 100644 --- a/tests/framework/utils/k8s_helper.go +++ b/tests/framework/utils/k8s_helper.go @@ -44,6 +44,7 @@ const ( cmd = "kubectl" RetryInterval = 2 RetryLoop = 100 + CronJobRetryLoop = 300 // CronJobs need more time: schedule delay + scan execution (600s) SkipVersionCheckEnvVar = "SKIP_VERSION_CHECK" ) @@ -221,7 +222,8 @@ func (k8sh *K8sHelper) WaitUntilMondooClientSecretExists(ctx context.Context, ns } // WaitUntilCronJobsSuccessful waits for the CronJobs with the specified selector to have at least -// one successful run. +// one successful run. Uses CronJobRetryLoop for a longer timeout since CronJobs need time for +// schedule delay plus scan execution. func (k8sh *K8sHelper) WaitUntilCronJobsSuccessful(labelSelector, namespace string) bool { listOpts, err := LabelSelectorListOptions(labelSelector) if err != nil { @@ -231,25 +233,26 @@ func (k8sh *K8sHelper) WaitUntilCronJobsSuccessful(labelSelector, namespace stri ctx := context.Background() cronJobs := &batchv1.CronJobList{} - err = k8sh.ExecuteWithRetries(func() (bool, error) { + for i := 0; i < CronJobRetryLoop; i++ { if err := k8sh.Clientset.List(ctx, cronJobs, listOpts); err != nil { zap.S().Errorf("Failed to list CronJobs. %v", err) - return false, err + return false } + + allReady := true for _, c := range cronJobs.Items { - // Make sure the job has been scheduled and is not active if c.Status.LastScheduleTime == nil || len(c.Status.Active) > 0 { - return false, nil + allReady = false + break } } - // Make sure all jobs have succeeded - if k8s.AreCronJobsSuccessful(cronJobs.Items) { - return true, nil + if allReady && k8s.AreCronJobsSuccessful(cronJobs.Items) { + return true } - return false, nil - }) - return err == nil + time.Sleep(RetryInterval * time.Second) + } + return false } func LabelSelectorListOptions(labelSelector string) (*client.ListOptions, error) { @@ -505,7 +508,8 @@ func (k8sh *K8sHelper) getPodDescribe(namespace string, args ...string) string { args = append([]string{"get", "pod", "-o", "yaml", "-n", namespace}, args...) description, err := k8sh.Kubectl(args...) if err != nil { - zap.S().Errorf("failed to describe pod. %v %+v", args, err) + // Completed CronJob pods may be cleaned up before we can describe them + zap.S().Warnf("failed to describe pod. %v %+v", args, err) return "" } return description diff --git a/tests/integration/audit_config_base_suite.go b/tests/integration/audit_config_base_suite.go index 1b0fee2ab..5c4454a53 100644 --- a/tests/integration/audit_config_base_suite.go +++ b/tests/integration/audit_config_base_suite.go @@ -120,7 +120,7 @@ func (s *AuditConfigBaseSuite) AfterTest(suiteName, testName string) { _, err = s.testCluster.K8sHelper.Kubectl("delete", "deployments", "-n", "default", "--all", "--ignore-not-found", "--wait") s.Require().NoError(err, "Failed to delete all deployments in default namespace") - _, err = s.testCluster.K8sHelper.Kubectl("delete", "pods", "-n", "default", "--all", "--wait") + _, err = s.testCluster.K8sHelper.Kubectl("delete", "pods", "-n", "default", "--all", "--ignore-not-found", "--wait") s.Require().NoError(err, "Failed to delete all pods") } } diff --git a/tests/integration/external_cluster_test.go b/tests/integration/external_cluster_test.go index cbb127a8d..ac762bb90 100644 --- a/tests/integration/external_cluster_test.go +++ b/tests/integration/external_cluster_test.go @@ -159,6 +159,9 @@ func (s *ExternalClusterSuite) switchToManagementContext() error { } func (s *ExternalClusterSuite) createTargetCluster() error { + // Delete any stale target cluster from a previous run + s.cleanupTargetCluster() + cmd := exec.Command("k3d", "cluster", "create", targetClusterName, "--api-port", "6444") output, err := cmd.CombinedOutput() if err != nil { From d8af2ebdff2888bae5c793506cd2027b86758897 Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Sat, 14 Feb 2026 18:39:49 +0100 Subject: [PATCH 9/9] fix: add expected words for spell checker Co-Authored-By: Claude Opus 4.6 --- .github/actions/spelling/expect.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index c8dc05f11..76027123b 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -1,20 +1,24 @@ AADSTS +artifactory bak bitnami curlimages deepcopy deletecollection +dockerconfigjson eksctl fullname iamidentitymapping irsa kustomization mcr +mondoooperatorconfig oidc openssl psat rolearn selfsigned +servicemonitor servicemonitors spiffe SVIDs