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
7 changed files with 93 additions and 3 deletions
Showing only changes of commit 0510a8151c - Show all commits

Address critical Prowler IaC findings via mute + RBAC tightening

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) <noreply@anthropic.com>
Erich Blume 2026-04-27 12:48:54 -07:00

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

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

View file

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

View file

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

View file

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

View file

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