From a76e471d5497c1083ff51a081ca30ec8fd0d9903 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 30 Mar 2026 17:22:31 -0700 Subject: [PATCH] Add Prowler mutelist and fix kube-state-metrics seccomp (#319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add mutelist files to suppress expected/accepted Prowler CIS findings from components we don't control - Mutelist files stored in `mutelist/` directory, grouped by category, merged at runtime via initContainer - Fix missing seccomp `RuntimeDefault` profile on kube-state-metrics deployment ### Mutelist categories | File | Checks | Covers | |------|--------|--------| | `apiserver.yaml` | 12 | Minikube apiserver flags | | `control-plane.yaml` | 3 | Scheduler, controller-manager, kubelet | | `core-pod-security.yaml` | 7 | System pods, Tailscale operator, Grafana init, Prowler hostPID, forgejo-runner | | `rbac.yaml` | 3 | Built-in K8s roles, ArgoCD, CNPG | Muted findings appear as `status=MUTED` in reports (not hidden), preserving audit trail. ### Not muted (follow-up) - Alloy, Immich pods missing seccomp — need separate investigation (Helm/operator-managed) ## Test plan - [ ] `kubectl kustomize argocd/manifests/prowler/` renders cleanly - [ ] Trigger manual scan: `kubectl --context=minikube-indri -n prowler create job prowler-mutelist-test --from=cronjob/prowler` - [ ] Verify initContainer merges successfully (check pod logs) - [ ] Verify muted findings show as `MUTED` in report - [ ] Sync kube-state-metrics and verify pod starts with seccomp profile 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.eblu.me/eblume/blumeops/pulls/319 --- .../kube-state-metrics/deployment.yaml | 2 + argocd/manifests/prowler/cronjob.yaml | 32 +++++++ argocd/manifests/prowler/kustomization.yaml | 10 +++ .../manifests/prowler/mutelist/apiserver.yaml | 54 ++++++++++++ .../prowler/mutelist/control-plane.yaml | 18 ++++ .../prowler/mutelist/core-pod-security.yaml | 86 +++++++++++++++++++ argocd/manifests/prowler/mutelist/rbac.yaml | 37 ++++++++ docs/changelog.d/prowler-mutelist.infra.md | 1 + 8 files changed, 240 insertions(+) create mode 100644 argocd/manifests/prowler/mutelist/apiserver.yaml create mode 100644 argocd/manifests/prowler/mutelist/control-plane.yaml create mode 100644 argocd/manifests/prowler/mutelist/core-pod-security.yaml create mode 100644 argocd/manifests/prowler/mutelist/rbac.yaml create mode 100644 docs/changelog.d/prowler-mutelist.infra.md diff --git a/argocd/manifests/kube-state-metrics/deployment.yaml b/argocd/manifests/kube-state-metrics/deployment.yaml index ae34339..ddaf3e2 100644 --- a/argocd/manifests/kube-state-metrics/deployment.yaml +++ b/argocd/manifests/kube-state-metrics/deployment.yaml @@ -51,3 +51,5 @@ spec: capabilities: drop: - ALL + seccompProfile: + type: RuntimeDefault diff --git a/argocd/manifests/prowler/cronjob.yaml b/argocd/manifests/prowler/cronjob.yaml index 545a9c8..5b2199b 100644 --- a/argocd/manifests/prowler/cronjob.yaml +++ b/argocd/manifests/prowler/cronjob.yaml @@ -15,6 +15,28 @@ spec: securityContext: seccompProfile: type: RuntimeDefault + initContainers: + - name: merge-mutelist + image: registry.ops.eblu.me/blumeops/prowler:kustomized + command: ["python3", "-c"] + args: + - | + import yaml, glob, pathlib + merged = {"Mutelist": {"Accounts": {"*": {"Checks": {}}}}} + for f in sorted(glob.glob("/mutelist-parts/*.yaml")): + with open(f) as fh: + data = yaml.safe_load(fh) + checks = data.get("Mutelist", {}).get("Accounts", {}).get("*", {}).get("Checks", {}) + merged["Mutelist"]["Accounts"]["*"]["Checks"].update(checks) + pathlib.Path("/tmp/mutelist").mkdir(exist_ok=True) + with open("/tmp/mutelist/mutelist.yaml", "w") as fh: + yaml.dump(merged, fh, default_flow_style=False) + print(f"Merged {len(merged['Mutelist']['Accounts']['*']['Checks'])} checks from {len(glob.glob('/mutelist-parts/*.yaml'))} files") + volumeMounts: + - name: mutelist-parts + mountPath: /mutelist-parts + - name: mutelist-merged + mountPath: /tmp/mutelist containers: - name: prowler image: registry.ops.eblu.me/blumeops/prowler:kustomized @@ -22,6 +44,8 @@ spec: - kubernetes - --compliance - cis_1.11_kubernetes + - --mutelist-file + - /tmp/mutelist/mutelist.yaml - -z - --output-formats - html @@ -32,6 +56,9 @@ spec: volumeMounts: - name: reports mountPath: /reports + - name: mutelist-merged + mountPath: /tmp/mutelist + readOnly: true - name: var-lib-kubelet mountPath: /var/lib/kubelet readOnly: true @@ -47,6 +74,11 @@ spec: - name: reports persistentVolumeClaim: claimName: prowler-reports + - name: mutelist-parts + configMap: + name: prowler-mutelist + - name: mutelist-merged + emptyDir: {} - name: var-lib-kubelet hostPath: path: /var/lib/kubelet diff --git a/argocd/manifests/prowler/kustomization.yaml b/argocd/manifests/prowler/kustomization.yaml index b34b2c1..162a2ad 100644 --- a/argocd/manifests/prowler/kustomization.yaml +++ b/argocd/manifests/prowler/kustomization.yaml @@ -13,6 +13,16 @@ resources: - cronjob-image-scan.yaml - cronjob-iac-scan.yaml +configMapGenerator: + - name: prowler-mutelist + options: + disableNameSuffixHash: true + files: + - mutelist/apiserver.yaml + - mutelist/control-plane.yaml + - mutelist/core-pod-security.yaml + - mutelist/rbac.yaml + images: - name: registry.ops.eblu.me/blumeops/prowler newTag: v5.22.0-6960243 diff --git a/argocd/manifests/prowler/mutelist/apiserver.yaml b/argocd/manifests/prowler/mutelist/apiserver.yaml new file mode 100644 index 0000000..a48c249 --- /dev/null +++ b/argocd/manifests/prowler/mutelist/apiserver.yaml @@ -0,0 +1,54 @@ +# Minikube apiserver — flags managed by static pod manifests. +# Compensating control: cluster not internet-exposed; access via Tailscale ACLs. +Mutelist: + Accounts: + "*": + Checks: + "apiserver_always_pull_images_plugin": + Regions: ["*"] + Resources: ["^kube-apiserver-minikube$"] + Description: "Minikube default; AlwaysPullImages not enabled." + "apiserver_audit_log_maxage_set": + Regions: ["*"] + Resources: ["^kube-apiserver-minikube$"] + Description: "Minikube does not configure audit logging." + "apiserver_audit_log_maxbackup_set": + Regions: ["*"] + Resources: ["^kube-apiserver-minikube$"] + Description: "Minikube does not configure audit logging." + "apiserver_audit_log_maxsize_set": + Regions: ["*"] + Resources: ["^kube-apiserver-minikube$"] + Description: "Minikube does not configure audit logging." + "apiserver_audit_log_path_set": + Regions: ["*"] + Resources: ["^kube-apiserver-minikube$"] + Description: "Minikube does not configure audit logging." + "apiserver_deny_service_external_ips": + Regions: ["*"] + Resources: ["^kube-apiserver-minikube$"] + Description: "Minikube default; no external IPs in use." + "apiserver_disable_profiling": + Regions: ["*"] + Resources: ["^kube-apiserver-minikube$"] + Description: "Minikube default; profiling endpoint not exposed." + "apiserver_encryption_provider_config_set": + Regions: ["*"] + Resources: ["^kube-apiserver-minikube$"] + Description: "Minikube does not configure etcd encryption at rest." + "apiserver_kubelet_cert_auth": + Regions: ["*"] + Resources: ["^kube-apiserver-minikube$"] + Description: "Minikube manages kubelet certificates automatically." + "apiserver_request_timeout_set": + Regions: ["*"] + Resources: ["^kube-apiserver-minikube$"] + Description: "Minikube default; using K8s default timeout." + "apiserver_service_account_lookup_true": + Regions: ["*"] + Resources: ["^kube-apiserver-minikube$"] + Description: "Minikube default." + "apiserver_strong_ciphers_only": + Regions: ["*"] + Resources: ["^kube-apiserver-minikube$"] + Description: "Minikube default TLS cipher suite." diff --git a/argocd/manifests/prowler/mutelist/control-plane.yaml b/argocd/manifests/prowler/mutelist/control-plane.yaml new file mode 100644 index 0000000..95e01bc --- /dev/null +++ b/argocd/manifests/prowler/mutelist/control-plane.yaml @@ -0,0 +1,18 @@ +# Minikube control-plane components — managed by static pod manifests. +# Compensating control: cluster not internet-exposed; access via Tailscale ACLs. +Mutelist: + Accounts: + "*": + Checks: + "controllermanager_disable_profiling": + Regions: ["*"] + Resources: ["^kube-controller-manager-minikube$"] + Description: "Minikube default; profiling endpoint not exposed outside tailnet." + "scheduler_profiling": + Regions: ["*"] + Resources: ["^kube-scheduler-minikube$"] + Description: "Minikube default; profiling endpoint not exposed outside tailnet." + "kubelet_tls_cert_and_key": + Regions: ["*"] + Resources: ["^kubelet-config$"] + Description: "Minikube uses auto-generated kubelet certificates." diff --git a/argocd/manifests/prowler/mutelist/core-pod-security.yaml b/argocd/manifests/prowler/mutelist/core-pod-security.yaml new file mode 100644 index 0000000..2c2169b --- /dev/null +++ b/argocd/manifests/prowler/mutelist/core-pod-security.yaml @@ -0,0 +1,86 @@ +# Pod security checks — system pods, operator-managed pods, and accepted +# operational needs. Each check ID appears once with all matching resources. +Mutelist: + Accounts: + "*": + Checks: + "core_minimize_hostNetwork_containers": + Regions: ["*"] + Resources: + # Minikube control plane — requires hostNetwork by design + - "^etcd-minikube$" + - "^kube-apiserver-minikube$" + - "^kube-controller-manager-minikube$" + - "^kube-scheduler-minikube$" + # Minikube system pods + - "^kube-proxy-" + - "^kindnet-" + - "^storage-provisioner$" + Description: >- + Control-plane and networking pods require hostNetwork. + All managed by minikube. + "core_minimize_privileged_containers": + Regions: ["*"] + Resources: + # Minikube system + - "^kube-proxy-" + # Tailscale operator-managed proxies + - "^ts-" + - "^ingress-" + # Forgejo runner — Docker-in-Docker for CI builds + - "^forgejo-runner-" + Description: >- + kube-proxy: iptables (minikube). ts-*/ingress-*: network + namespace manipulation (Tailscale operator). forgejo-runner: + Docker-in-Docker for CI. + "core_seccomp_profile_docker_default": + Regions: ["*"] + Resources: + # Minikube system pods + - "^coredns-" + - "^kube-proxy-" + - "^kindnet-" + - "^storage-provisioner$" + # Tailscale operator-managed pods + - "^ts-" + - "^operator-" + - "^nameserver-" + - "^ingress-" + Description: >- + System pods (minikube) and Tailscale operator pods — seccomp + profiles set by upstream/operator, not user manifests. + "core_minimize_hostPID_containers": + Regions: ["*"] + Resources: + - "^prowler-" + Description: >- + Prowler CIS scanner requires hostPID to check file + permissions on kubelet and etcd data directories. + "core_minimize_root_containers_admission": + Regions: ["*"] + Resources: + - "^grafana-" + Description: >- + Grafana init-chown-data runs as root to fix PVC ownership. + Main containers run as UID 472. Standard pattern. + "core_minimize_containers_added_capabilities": + Regions: ["*"] + Resources: + # Minikube system pods + - "^coredns-" + - "^kindnet-" + # Grafana init-chown-data (CHOWN capability) + - "^grafana-" + Description: >- + System pods: NET_BIND_SERVICE/NET_RAW required by function + (minikube). Grafana: CHOWN for PVC init; all other + containers drop ALL. + "core_minimize_containers_capabilities_assigned": + Regions: ["*"] + Resources: + - "^coredns-" + - "^kindnet-" + - "^grafana-" + Description: >- + System pods (minikube) and Grafana init-chown-data. + See core_minimize_containers_added_capabilities. diff --git a/argocd/manifests/prowler/mutelist/rbac.yaml b/argocd/manifests/prowler/mutelist/rbac.yaml new file mode 100644 index 0000000..c5d0ceb --- /dev/null +++ b/argocd/manifests/prowler/mutelist/rbac.yaml @@ -0,0 +1,37 @@ +# RBAC checks — built-in Kubernetes roles and operator roles that require +# broad permissions by design. +Mutelist: + Accounts: + "*": + Checks: + "rbac_minimize_wildcard_use_roles": + Regions: ["*"] + Resources: + # Built-in Kubernetes roles + - "^cluster-admin$" + - "^system:" + # ArgoCD — requires broad access for deployment management; + # ArgoCD itself is SSO-gated via Authentik + - "^argocd-" + Description: >- + Built-in K8s roles and ArgoCD. ArgoCD access is SSO-gated + via Authentik. + "rbac_minimize_pod_creation_access": + Regions: ["*"] + Resources: + # Built-in Kubernetes roles + - "^admin$" + - "^edit$" + - "^system:" + # CloudNativePG operator + - "^cnpg-manager$" + Description: >- + Built-in K8s roles required for workload controllers. + cnpg-manager: CloudNativePG operator manages PostgreSQL pods. + "rbac_minimize_service_account_token_creation": + Regions: ["*"] + Resources: + - "^system:" + Description: >- + kube-controller-manager requires token creation for service + account management. Built-in role. diff --git a/docs/changelog.d/prowler-mutelist.infra.md b/docs/changelog.d/prowler-mutelist.infra.md new file mode 100644 index 0000000..a8bf246 --- /dev/null +++ b/docs/changelog.d/prowler-mutelist.infra.md @@ -0,0 +1 @@ +Add Prowler mutelist to suppress expected findings from system components, operator-managed pods, and accepted operational needs. Fix missing seccomp profile on kube-state-metrics.