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

## Summary

The weekly Prowler IaC scan reported 6 critical findings against `argocd/manifests/`. They split cleanly into two patterns:

- **Legitimate-by-design RBAC → mute with new compensating controls**
  - `external-secrets-controller`, `external-secrets-cert-controller` manage `secrets` (KSV-0041) and the cert-controller mutates its own webhook configurations (KSV-0114). This is what the operator is *for*. New CC: `operator-purpose-bound-rbac`.
  - `kube-state-metrics` (both `minikube-indri` and `k3s-ringtail`) holds `list/watch` on secrets to expose `kube_secret_info` and `kube_secret_labels` metrics. KSM's metric schema only reads metadata, never the `data:` field. New CC: `kube-state-metrics-metadata-only`.

- **Over-broad RBAC → fix**
  - `grafana-clusterrole` had `get/watch/list` on `secrets` because the dashboard-sidecar config used `RESOURCE=both` (ConfigMaps + Secrets). Nothing in the cluster labels Secrets with `grafana_dashboard=1`, so this was unused power. Switched both sidecar instances to `RESOURCE=configmap` and removed `secrets` from the ClusterRole.

The IaC cronjob also did not previously pass `--mutelist-file`, which is why every IaC finding reported as unmuted regardless of mutelist configuration. The new `mutelist/iac.yaml` is bundled into the existing `prowler-mutelist` ConfigMap and mounted via `items:` selector.

## Test plan

- [ ] `kubectl --context=minikube-indri kustomize argocd/manifests/prowler/` — already passes locally
- [ ] `kubectl --context=minikube-indri kustomize argocd/manifests/grafana/` — already passes locally
- [ ] Deploy from this branch via `argocd app set prowler --revision prowler-iac-mutelist && argocd app sync prowler` and same for `grafana`
- [ ] Manually trigger the IaC cronjob and verify `MUTED=True` on the 6 critical findings (`kubectl --context=minikube-indri -n prowler create job --from=cronjob/prowler-iac-scan prowler-iac-test`)
- [ ] Restart grafana pod and confirm dashboards still render (sidecar still finds them via ConfigMap watch)
- [ ] After verify, `argocd app set <app> --revision main && argocd app sync <app>` post-merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #340
This commit is contained in:
Erich Blume 2026-04-29 10:43:32 -07:00
commit 495e45d01d
8 changed files with 118 additions and 5 deletions

View file

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

View file

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

View file

@ -19,6 +19,13 @@ 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)
@ -31,8 +38,17 @@ spec:
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: trivyignore.yaml
path: trivyignore.yaml

View file

@ -23,7 +23,8 @@ configMapGenerator:
- mutelist/core-pod-security.yaml
- mutelist/manual-node-checks.yaml
- mutelist/rbac.yaml
- mutelist/trivyignore.yaml
images:
- 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.