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