From 0510a8151c5130df6d296f0947c77105fd3c1b38 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 27 Apr 2026 12:48:54 -0700 Subject: [PATCH 1/3] Address critical Prowler IaC findings via mute + RBAC tightening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six critical IaC findings against argocd/manifests/ broke into two patterns: legitimate-by-design RBAC (mute) and over-broad RBAC (fix). Plumbing: - cronjob-iac-scan.yaml now passes --mutelist-file (previously unused, which is why all IaC findings reported as unmuted) - new mutelist/iac.yaml is bundled into the prowler-mutelist ConfigMap and mounted into the IaC cronjob via items: selector Compensating controls (in compensating-controls.yaml): - operator-purpose-bound-rbac — external-secrets-operator's whole function is to manage Secret objects; ClusterRole over secrets matches its purpose. cert-controller mutates its own validating webhooks to inject a rotating CA bundle. - kube-state-metrics-metadata-only — KSM exposes only Secret metadata via kube_secret_info / kube_secret_labels; the data field is never read into exposed metrics. Mutes (mutelist/iac.yaml): - KSV-0041 for external-secrets/rbac.yaml, kube-state-metrics/rbac.yaml, kube-state-metrics-ringtail/rbac.yaml - KSV-0114 for external-secrets/rbac.yaml Real fix: - grafana-clusterrole no longer reads secrets. The dashboard sidecar (RESOURCE=both → configmap, both init and watch instances) only needs ConfigMap-labeled dashboards; no Secrets are labeled grafana_dashboard. Co-Authored-By: Claude Opus 4.7 (1M context) --- argocd/manifests/grafana/deployment.yaml | 6 ++- argocd/manifests/grafana/rbac.yaml | 2 +- .../manifests/prowler/cronjob-iac-scan.yaml | 10 +++++ argocd/manifests/prowler/kustomization.yaml | 1 + argocd/manifests/prowler/mutelist/iac.yaml | 40 +++++++++++++++++++ compensating-controls.yaml | 36 +++++++++++++++++ .../changelog.d/prowler-iac-mutelist.infra.md | 1 + 7 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 argocd/manifests/prowler/mutelist/iac.yaml create mode 100644 docs/changelog.d/prowler-iac-mutelist.infra.md diff --git a/argocd/manifests/grafana/deployment.yaml b/argocd/manifests/grafana/deployment.yaml index 848503e..0aad9b3 100644 --- a/argocd/manifests/grafana/deployment.yaml +++ b/argocd/manifests/grafana/deployment.yaml @@ -156,7 +156,9 @@ spec: - name: FOLDER value: /tmp/dashboards - name: RESOURCE - value: both + # ConfigMap-only — no dashboards are sourced from Secrets, + # so the ServiceAccount has no read access to secrets. + value: configmap - name: FOLDER_ANNOTATION value: grafana_folder securityContext: @@ -183,7 +185,7 @@ spec: - name: FOLDER value: /tmp/dashboards - name: RESOURCE - value: both + value: configmap - name: FOLDER_ANNOTATION value: grafana_folder - name: REQ_USERNAME diff --git a/argocd/manifests/grafana/rbac.yaml b/argocd/manifests/grafana/rbac.yaml index d0d0c843..1c2dee3 100644 --- a/argocd/manifests/grafana/rbac.yaml +++ b/argocd/manifests/grafana/rbac.yaml @@ -7,7 +7,7 @@ metadata: app.kubernetes.io/instance: grafana rules: - apiGroups: [""] - resources: ["configmaps", "secrets"] + resources: ["configmaps"] verbs: ["get", "watch", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/argocd/manifests/prowler/cronjob-iac-scan.yaml b/argocd/manifests/prowler/cronjob-iac-scan.yaml index 49c8ce6..27c7c0b 100644 --- a/argocd/manifests/prowler/cronjob-iac-scan.yaml +++ b/argocd/manifests/prowler/cronjob-iac-scan.yaml @@ -25,14 +25,24 @@ spec: mkdir -p "$DATEDIR" prowler iac \ --scan-repository-url https://forge.ops.eblu.me/eblume/blumeops.git \ + --mutelist-file /mutelist/iac.yaml \ -z \ --output-formats html csv json-ocsf \ --output-directory "$DATEDIR" volumeMounts: - name: reports mountPath: /reports + - name: mutelist + mountPath: /mutelist + readOnly: true restartPolicy: OnFailure volumes: - name: reports persistentVolumeClaim: claimName: prowler-reports + - name: mutelist + configMap: + name: prowler-mutelist + items: + - key: iac.yaml + path: iac.yaml diff --git a/argocd/manifests/prowler/kustomization.yaml b/argocd/manifests/prowler/kustomization.yaml index 7024aff..0d40035 100644 --- a/argocd/manifests/prowler/kustomization.yaml +++ b/argocd/manifests/prowler/kustomization.yaml @@ -23,6 +23,7 @@ configMapGenerator: - mutelist/core-pod-security.yaml - mutelist/manual-node-checks.yaml - mutelist/rbac.yaml + - mutelist/iac.yaml images: - name: registry.ops.eblu.me/blumeops/prowler diff --git a/argocd/manifests/prowler/mutelist/iac.yaml b/argocd/manifests/prowler/mutelist/iac.yaml new file mode 100644 index 0000000..a94f947 --- /dev/null +++ b/argocd/manifests/prowler/mutelist/iac.yaml @@ -0,0 +1,40 @@ +# IaC scan mutes — Trivy KSV checks against argocd/manifests/. +# +# Check ID format: "KSV-XXXX" (Trivy Kubernetes Security check IDs). +# Region / Resource semantics for Prowler IaC: Region == repo path, +# Resource == manifest file path (relative to repo root). +Mutelist: + Accounts: + "*": + Checks: + "KSV-0041": + # Mutelist entries under one CHECK_ID share a Resources list. + # Each resource here justifies muting under a distinct CC; see + # the per-resource notes below. + Regions: ["*"] + Resources: + # CC: operator-purpose-bound-rbac. external-secrets-operator's + # entire function is to read and synthesize Secret objects; + # ClusterRole over secrets is its purpose. Both the controller + # and cert-controller are upstream-defined. + - "argocd/manifests/external-secrets/rbac.yaml" + # CC: kube-state-metrics-metadata-only. KSM exposes only + # Secret metadata (name, namespace, type, labels), never the + # data field. list/watch on secrets is required to expose + # kube_secret_info and kube_secret_labels. + - "argocd/manifests/kube-state-metrics/rbac.yaml" + - "argocd/manifests/kube-state-metrics-ringtail/rbac.yaml" + Description: >- + CC: operator-purpose-bound-rbac (external-secrets); + kube-state-metrics-metadata-only (kube-state-metrics). + "KSV-0114": + Regions: ["*"] + Resources: + - "argocd/manifests/external-secrets/rbac.yaml" + Description: >- + CC: operator-purpose-bound-rbac. cert-controller manages the + external-secrets validating webhook configurations to inject + its own rotating CA bundle. RBAC is scoped to two named + webhooks (secretstore-validate, externalsecret-validate) via + resourceNames; KSV-0114 doesn't see the resourceNames + restriction so reports the full ClusterRole. diff --git a/compensating-controls.yaml b/compensating-controls.yaml index 67bbf75..d9d7c6c 100644 --- a/compensating-controls.yaml +++ b/compensating-controls.yaml @@ -139,6 +139,42 @@ controls: MANUAL findings appear in Prowler, add corresponding verification logic to the script and update the mutelist. + - id: operator-purpose-bound-rbac + description: >- + Operators whose entire function is to manage a sensitive resource + legitimately need RBAC over that resource. external-secrets-operator + manages Secret objects (its purpose) and the cert-controller mutates + its own ValidatingWebhookConfigurations to inject rotating CA bundles. + Risk is bounded by: (1) the operator code being upstream open-source + and reviewed; (2) RBAC scoped to specific named webhooks where + possible; (3) supply chain controls on the operator image (mirrored + to local registry, version tracked in service-versions.yaml). + created: 2026-04-27 + last-reviewed: 2026-04-27 + notes: >- + Verify by checking that the operators in question still match their + stated purpose (i.e. external-secrets is still the only consumer of + these ClusterRoles) and that upstream hasn't published advisories + for credential-handling bugs. Re-evaluate if a non-secrets-managing + ClusterRole appears under this control. + + - id: kube-state-metrics-metadata-only + description: >- + kube-state-metrics holds list/watch on Secrets cluster-wide but only + exposes Secret object *metadata* (name, namespace, type, creation + timestamp, labels) via the kube_secret_info / kube_secret_labels + metrics. Secret data fields are never read into KSM's exposed + metrics by upstream design. Mitigation rests on KSM's metric + schema, the version pin in service-versions.yaml, and the metrics + endpoint being reachable only on the cluster network. + created: 2026-04-27 + last-reviewed: 2026-04-27 + notes: >- + Verify by inspecting the /metrics endpoint output for any series + that include secret data (only *_info and *_labels metrics should + reference secrets, and labels should be limited to user-applied + labels — never the data:). Re-evaluate on KSM version bumps. + - id: observability-stack-audit description: >- Alloy collects pod logs and ships them to Loki, providing an diff --git a/docs/changelog.d/prowler-iac-mutelist.infra.md b/docs/changelog.d/prowler-iac-mutelist.infra.md new file mode 100644 index 0000000..ea573b9 --- /dev/null +++ b/docs/changelog.d/prowler-iac-mutelist.infra.md @@ -0,0 +1 @@ +Address the 6 critical Prowler IaC findings against `argocd/manifests/`. The IaC cronjob now passes `--mutelist-file` (previously unused), fed from a new `mutelist/iac.yaml`. Two new compensating controls — `operator-purpose-bound-rbac` and `kube-state-metrics-metadata-only` — justify muting the `external-secrets` and `kube-state-metrics` Secret-access findings (KSV-0041, KSV-0114). Separately, `grafana-clusterrole` is tightened to remove `secrets` access entirely: the dashboard sidecar already only consumes ConfigMap-labeled dashboards, so its `RESOURCE` env var is now `configmap` instead of `both`. From 2daf6291b79397185db8de02abbefd12dba1cd97 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Tue, 28 Apr 2026 09:50:31 -0700 Subject: [PATCH 2/3] Replace dead Prowler IaC mutelist with Trivy ignorefile shim Prowler's IaC provider hardcodes self._mutelist = None and delegates filtering to Trivy, but doesn't plumb --ignorefile through. The original attempt with --mutelist-file silently no-op'd. Add a wrapper around trivy in our image that injects --ignorefile $TRIVY_IGNOREFILE on `fs` subcommands; switch the IaC cronjob to mount a Trivy-format trivyignore.yaml and set the env var. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../manifests/prowler/cronjob-iac-scan.yaml | 12 ++++-- argocd/manifests/prowler/kustomization.yaml | 2 +- argocd/manifests/prowler/mutelist/iac.yaml | 40 ------------------- .../prowler/mutelist/trivyignore.yaml | 39 ++++++++++++++++++ containers/prowler/Dockerfile | 20 +++++++++- .../changelog.d/prowler-iac-mutelist.infra.md | 2 +- 6 files changed, 69 insertions(+), 46 deletions(-) delete mode 100644 argocd/manifests/prowler/mutelist/iac.yaml create mode 100644 argocd/manifests/prowler/mutelist/trivyignore.yaml diff --git a/argocd/manifests/prowler/cronjob-iac-scan.yaml b/argocd/manifests/prowler/cronjob-iac-scan.yaml index 27c7c0b..c1303a5 100644 --- a/argocd/manifests/prowler/cronjob-iac-scan.yaml +++ b/argocd/manifests/prowler/cronjob-iac-scan.yaml @@ -19,13 +19,19 @@ spec: - name: prowler image: registry.ops.eblu.me/blumeops/prowler:kustomized command: ["/bin/sh", "-c"] + # Prowler's --mutelist-file is a no-op for the IaC provider + # (it delegates to Trivy). The Prowler image's trivy shim + # injects --ignorefile $TRIVY_IGNOREFILE when set; see + # containers/prowler/Dockerfile. + env: + - name: TRIVY_IGNOREFILE + value: /mutelist/trivyignore.yaml args: - | DATEDIR=/reports/prowler-iac/$(date +%Y-%m-%d) mkdir -p "$DATEDIR" prowler iac \ --scan-repository-url https://forge.ops.eblu.me/eblume/blumeops.git \ - --mutelist-file /mutelist/iac.yaml \ -z \ --output-formats html csv json-ocsf \ --output-directory "$DATEDIR" @@ -44,5 +50,5 @@ spec: configMap: name: prowler-mutelist items: - - key: iac.yaml - path: iac.yaml + - key: trivyignore.yaml + path: trivyignore.yaml diff --git a/argocd/manifests/prowler/kustomization.yaml b/argocd/manifests/prowler/kustomization.yaml index 0d40035..f1ed931 100644 --- a/argocd/manifests/prowler/kustomization.yaml +++ b/argocd/manifests/prowler/kustomization.yaml @@ -23,7 +23,7 @@ configMapGenerator: - mutelist/core-pod-security.yaml - mutelist/manual-node-checks.yaml - mutelist/rbac.yaml - - mutelist/iac.yaml + - mutelist/trivyignore.yaml images: - name: registry.ops.eblu.me/blumeops/prowler diff --git a/argocd/manifests/prowler/mutelist/iac.yaml b/argocd/manifests/prowler/mutelist/iac.yaml deleted file mode 100644 index a94f947..0000000 --- a/argocd/manifests/prowler/mutelist/iac.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# IaC scan mutes — Trivy KSV checks against argocd/manifests/. -# -# Check ID format: "KSV-XXXX" (Trivy Kubernetes Security check IDs). -# Region / Resource semantics for Prowler IaC: Region == repo path, -# Resource == manifest file path (relative to repo root). -Mutelist: - Accounts: - "*": - Checks: - "KSV-0041": - # Mutelist entries under one CHECK_ID share a Resources list. - # Each resource here justifies muting under a distinct CC; see - # the per-resource notes below. - Regions: ["*"] - Resources: - # CC: operator-purpose-bound-rbac. external-secrets-operator's - # entire function is to read and synthesize Secret objects; - # ClusterRole over secrets is its purpose. Both the controller - # and cert-controller are upstream-defined. - - "argocd/manifests/external-secrets/rbac.yaml" - # CC: kube-state-metrics-metadata-only. KSM exposes only - # Secret metadata (name, namespace, type, labels), never the - # data field. list/watch on secrets is required to expose - # kube_secret_info and kube_secret_labels. - - "argocd/manifests/kube-state-metrics/rbac.yaml" - - "argocd/manifests/kube-state-metrics-ringtail/rbac.yaml" - Description: >- - CC: operator-purpose-bound-rbac (external-secrets); - kube-state-metrics-metadata-only (kube-state-metrics). - "KSV-0114": - Regions: ["*"] - Resources: - - "argocd/manifests/external-secrets/rbac.yaml" - Description: >- - CC: operator-purpose-bound-rbac. cert-controller manages the - external-secrets validating webhook configurations to inject - its own rotating CA bundle. RBAC is scoped to two named - webhooks (secretstore-validate, externalsecret-validate) via - resourceNames; KSV-0114 doesn't see the resourceNames - restriction so reports the full ClusterRole. diff --git a/argocd/manifests/prowler/mutelist/trivyignore.yaml b/argocd/manifests/prowler/mutelist/trivyignore.yaml new file mode 100644 index 0000000..22c612a --- /dev/null +++ b/argocd/manifests/prowler/mutelist/trivyignore.yaml @@ -0,0 +1,39 @@ +# Trivy ignorefile for Prowler IaC scan. +# +# Prowler's `--mutelist-file` flag is a no-op for the IaC provider +# (iac_provider.py sets self._mutelist = None and delegates to Trivy). +# Trivy in turn does not auto-discover this YAML form from cwd, so the +# Prowler image ships a shim wrapper around `trivy` that injects +# --ignorefile $TRIVY_IGNOREFILE when the env var is set. The cronjob +# mounts this file and sets TRIVY_IGNOREFILE accordingly. +# +# Schema: https://trivy.dev/latest/docs/configuration/filtering/ +# IDs use the hyphenated form Trivy displays (KSV-0041, not KSV0041). +misconfigurations: + - id: KSV-0041 + paths: + - "argocd/manifests/external-secrets/rbac.yaml" + statement: >- + CC: operator-purpose-bound-rbac. external-secrets-operator's entire + function is to read and synthesize Secret objects; ClusterRole over + secrets is its purpose. Both the controller and cert-controller are + upstream-defined. + - id: KSV-0041 + paths: + - "argocd/manifests/kube-state-metrics/rbac.yaml" + - "argocd/manifests/kube-state-metrics-ringtail/rbac.yaml" + statement: >- + CC: kube-state-metrics-metadata-only. KSM exposes only Secret + metadata (name, namespace, type, labels), never the data field. + list/watch on secrets is required for kube_secret_info / + kube_secret_labels metrics. + - id: KSV-0114 + paths: + - "argocd/manifests/external-secrets/rbac.yaml" + statement: >- + CC: operator-purpose-bound-rbac. cert-controller manages the + external-secrets validating webhook configurations to inject its + own rotating CA bundle. RBAC is scoped to two named webhooks + (secretstore-validate, externalsecret-validate) via resourceNames; + KSV-0114 doesn't see the resourceNames restriction so reports the + full ClusterRole. diff --git a/containers/prowler/Dockerfile b/containers/prowler/Dockerfile index bd74bdb..c5157cb 100644 --- a/containers/prowler/Dockerfile +++ b/containers/prowler/Dockerfile @@ -44,10 +44,28 @@ RUN ARCH=$(dpkg --print-architecture) \ && apt-get update && apt-get install -y --no-install-recommends wget ca-certificates \ && wget -q "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_${TRIVY_ARCH}.tar.gz" -O /tmp/trivy.tar.gz \ && tar xzf /tmp/trivy.tar.gz -C /usr/local/bin trivy \ - && chmod +x /usr/local/bin/trivy \ + && mv /usr/local/bin/trivy /usr/local/bin/trivy.real \ + && chmod +x /usr/local/bin/trivy.real \ && rm /tmp/trivy.tar.gz \ && apt-get purge -y wget && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* +# Shim: Prowler's IaC provider invokes `trivy fs` directly with no +# --ignorefile flag, so any TRIVY_IGNOREFILE the user sets is ignored. +# This wrapper injects --ignorefile when the env var points at a real +# file and the invocation is `trivy fs ...`. Other subcommands and +# global-only invocations (--version, --help) pass through unchanged. +# TODO(upstream): contribute --ignorefile plumbing to prowler-cloud/prowler +# iac_provider.py so this shim isn't necessary. +RUN printf '%s\n' \ + '#!/bin/sh' \ + 'if [ "${1:-}" = "fs" ] && [ -n "${TRIVY_IGNOREFILE:-}" ] && [ -f "${TRIVY_IGNOREFILE}" ]; then' \ + ' shift' \ + ' exec /usr/local/bin/trivy.real fs --ignorefile "${TRIVY_IGNOREFILE}" "$@"' \ + 'fi' \ + 'exec /usr/local/bin/trivy.real "$@"' \ + > /usr/local/bin/trivy \ + && chmod +x /usr/local/bin/trivy + RUN addgroup --gid 1000 prowler \ && adduser --uid 1000 --gid 1000 --disabled-password --gecos "" prowler \ && mkdir -p /tmp/.cache/trivy && chown prowler:prowler /tmp/.cache/trivy diff --git a/docs/changelog.d/prowler-iac-mutelist.infra.md b/docs/changelog.d/prowler-iac-mutelist.infra.md index ea573b9..793c1ec 100644 --- a/docs/changelog.d/prowler-iac-mutelist.infra.md +++ b/docs/changelog.d/prowler-iac-mutelist.infra.md @@ -1 +1 @@ -Address the 6 critical Prowler IaC findings against `argocd/manifests/`. The IaC cronjob now passes `--mutelist-file` (previously unused), fed from a new `mutelist/iac.yaml`. Two new compensating controls — `operator-purpose-bound-rbac` and `kube-state-metrics-metadata-only` — justify muting the `external-secrets` and `kube-state-metrics` Secret-access findings (KSV-0041, KSV-0114). Separately, `grafana-clusterrole` is tightened to remove `secrets` access entirely: the dashboard sidecar already only consumes ConfigMap-labeled dashboards, so its `RESOURCE` env var is now `configmap` instead of `both`. +Address the 6 critical Prowler IaC findings against `argocd/manifests/`. Prowler's IaC provider hardcodes `self._mutelist = None` and delegates filtering to Trivy, but doesn't plumb `--ignorefile` through — so the documented "use Trivy filtering" path is actually broken. Added a shim around `trivy` in the Prowler image that injects `--ignorefile $TRIVY_IGNOREFILE` for `trivy fs` invocations when the env var points at a real file. The IaC cronjob now mounts `mutelist/trivyignore.yaml` (Trivy's per-path schema) and sets the env var. Two new compensating controls — `operator-purpose-bound-rbac` and `kube-state-metrics-metadata-only` — justify muting the `external-secrets` and `kube-state-metrics` Secret-access findings (KSV-0041, KSV-0114). Separately, `grafana-clusterrole` is tightened to remove `secrets` access entirely: the dashboard sidecar already only consumes ConfigMap-labeled dashboards, so its `RESOURCE` env var is now `configmap` instead of `both`. From b8d2c3c1ede151710c4765897ffd70a2d1644e78 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Tue, 28 Apr 2026 10:24:15 -0700 Subject: [PATCH 3/3] Bump prowler image to v5.23.0-2daf629 Includes the trivy ignorefile shim needed to make IaC mutes work. Co-Authored-By: Claude Opus 4.7 (1M context) --- argocd/manifests/prowler/kustomization.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/manifests/prowler/kustomization.yaml b/argocd/manifests/prowler/kustomization.yaml index f1ed931..cf644dc 100644 --- a/argocd/manifests/prowler/kustomization.yaml +++ b/argocd/manifests/prowler/kustomization.yaml @@ -27,4 +27,4 @@ configMapGenerator: images: - name: registry.ops.eblu.me/blumeops/prowler - newTag: v5.23.0-7c1cd11 + newTag: v5.23.0-2daf629