Address 6 critical Prowler IaC findings (mute + grafana RBAC tighten) #340

Merged
eblume merged 3 commits from prowler-iac-mutelist into main 2026-04-29 10:43:33 -07:00
8 changed files with 118 additions and 5 deletions

View file

@ -156,7 +156,9 @@ spec:
- name: FOLDER - name: FOLDER
value: /tmp/dashboards value: /tmp/dashboards
- name: RESOURCE - 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 - name: FOLDER_ANNOTATION
value: grafana_folder value: grafana_folder
securityContext: securityContext:
@ -183,7 +185,7 @@ spec:
- name: FOLDER - name: FOLDER
value: /tmp/dashboards value: /tmp/dashboards
- name: RESOURCE - name: RESOURCE
value: both value: configmap
- name: FOLDER_ANNOTATION - name: FOLDER_ANNOTATION
value: grafana_folder value: grafana_folder
- name: REQ_USERNAME - name: REQ_USERNAME

View file

@ -7,7 +7,7 @@ metadata:
app.kubernetes.io/instance: grafana app.kubernetes.io/instance: grafana
rules: rules:
- apiGroups: [""] - apiGroups: [""]
resources: ["configmaps", "secrets"] resources: ["configmaps"]
verbs: ["get", "watch", "list"] verbs: ["get", "watch", "list"]
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1

View file

@ -19,6 +19,13 @@ spec:
- name: prowler - name: prowler
image: registry.ops.eblu.me/blumeops/prowler:kustomized image: registry.ops.eblu.me/blumeops/prowler:kustomized
command: ["/bin/sh", "-c"] 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: args:
- | - |
DATEDIR=/reports/prowler-iac/$(date +%Y-%m-%d) DATEDIR=/reports/prowler-iac/$(date +%Y-%m-%d)
@ -31,8 +38,17 @@ spec:
volumeMounts: volumeMounts:
- name: reports - name: reports
mountPath: /reports mountPath: /reports
- name: mutelist
mountPath: /mutelist
readOnly: true
restartPolicy: OnFailure restartPolicy: OnFailure
volumes: volumes:
- name: reports - name: reports
persistentVolumeClaim: persistentVolumeClaim:
claimName: prowler-reports claimName: prowler-reports
- name: mutelist
configMap:
name: prowler-mutelist
items:
- key: trivyignore.yaml
path: trivyignore.yaml

View file

@ -23,7 +23,8 @@ configMapGenerator:
- mutelist/core-pod-security.yaml - mutelist/core-pod-security.yaml
- mutelist/manual-node-checks.yaml - mutelist/manual-node-checks.yaml
- mutelist/rbac.yaml - mutelist/rbac.yaml
- mutelist/trivyignore.yaml
images: images:
- name: registry.ops.eblu.me/blumeops/prowler - name: registry.ops.eblu.me/blumeops/prowler
newTag: v5.23.0-7c1cd11 newTag: v5.23.0-2daf629

View file

@ -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.

View file

@ -139,6 +139,42 @@ controls:
MANUAL findings appear in Prowler, add corresponding verification MANUAL findings appear in Prowler, add corresponding verification
logic to the script and update the mutelist. 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 - id: observability-stack-audit
description: >- description: >-
Alloy collects pod logs and ships them to Loki, providing an Alloy collects pod logs and ships them to Loki, providing an

View file

@ -44,10 +44,28 @@ RUN ARCH=$(dpkg --print-architecture) \
&& apt-get update && apt-get install -y --no-install-recommends wget ca-certificates \ && 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 \ && 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 \ && 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 \ && rm /tmp/trivy.tar.gz \
&& apt-get purge -y wget && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* && 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 \ RUN addgroup --gid 1000 prowler \
&& adduser --uid 1000 --gid 1000 --disabled-password --gecos "" prowler \ && adduser --uid 1000 --gid 1000 --disabled-password --gecos "" prowler \
&& mkdir -p /tmp/.cache/trivy && chown prowler:prowler /tmp/.cache/trivy && mkdir -p /tmp/.cache/trivy && chown prowler:prowler /tmp/.cache/trivy

View file

@ -0,0 +1 @@
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`.