Add compensating controls framework and date-based report dirs (#320)

## Summary

- Add `compensating-controls.yaml` tracking 9 named controls that justify suppressed security findings
- Update all Prowler mutelist descriptions with `CC: <id>` references to named controls
- Add `mise run review-compensating-controls` task — surfaces stalest control with all codebase references
- Add [[review-compensating-controls]] how-to doc
- Organize Prowler and Kingfisher reports into `YYYY-MM-DD` subdirectories

### Compensating controls

| ID | Mitigates |
|----|-----------|
| `single-user-cluster` | Image cache abuse, RBAC breadth, system pod privileges |
| `tailscale-network-isolation` | Profiling endpoints, weak TLS, debug ports |
| `local-registry` | AlwaysPullImages gap |
| `sso-gated-admin-tools` | ArgoCD wildcard RBAC |
| `operator-managed-pods` | Tailscale proxy pod security settings |
| `ephemeral-privileged-jobs` | Prowler hostPID exposure |
| `trusted-ci-only` | Forgejo runner DinD |
| `init-container-isolation` | Grafana root init container |
| `observability-stack-audit` | Missing apiserver audit logging |

## Test plan

- [ ] `mise run review-compensating-controls` shows table and references
- [ ] `kubectl kustomize argocd/manifests/prowler/` renders correctly
- [ ] Sync prowler and kingfisher, verify next scan writes to dated subdirectory
- [ ] Grep for `CC:` in mutelist files — every muted finding should have at least one

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

Reviewed-on: #320
This commit is contained in:
Erich Blume 2026-03-30 17:44:11 -07:00
commit 4059b3d27b
13 changed files with 516 additions and 77 deletions

View file

@ -23,7 +23,7 @@ spec:
- |
set -e
STAMP=$(date +%Y%m%d-%H%M%S)
OUTDIR=/reports/kingfisher
OUTDIR=/reports/kingfisher/$(date +%Y-%m-%d)
mkdir -p "$OUTDIR"
# Exit codes: 0=clean, 200=findings, 205=validated findings.

View file

@ -18,17 +18,16 @@ spec:
containers:
- name: prowler
image: registry.ops.eblu.me/blumeops/prowler:kustomized
command: ["/bin/sh", "-c"]
args:
- iac
- --scan-repository-url
- https://forge.ops.eblu.me/eblume/blumeops.git
- -z
- --output-formats
- html
- csv
- json-ocsf
- --output-directory
- /reports/prowler-iac
- |
DATEDIR=/reports/prowler-iac/$(date +%Y-%m-%d)
mkdir -p "$DATEDIR"
prowler iac \
--scan-repository-url https://forge.ops.eblu.me/eblume/blumeops.git \
-z \
--output-formats html csv json-ocsf \
--output-directory "$DATEDIR"
volumeMounts:
- name: reports
mountPath: /reports

View file

@ -48,17 +48,16 @@ spec:
containers:
- name: prowler
image: registry.ops.eblu.me/blumeops/prowler:kustomized
command: ["/bin/sh", "-c"]
args:
- image
- --image-list
- /shared/images.txt
- -z
- --output-formats
- html
- csv
- json-ocsf
- --output-directory
- /reports/prowler-images
- |
DATEDIR=/reports/prowler-images/$(date +%Y-%m-%d)
mkdir -p "$DATEDIR"
prowler image \
--image-list /shared/images.txt \
-z \
--output-formats html csv json-ocsf \
--output-directory "$DATEDIR"
volumeMounts:
- name: reports
mountPath: /reports

View file

@ -40,19 +40,17 @@ spec:
containers:
- name: prowler
image: registry.ops.eblu.me/blumeops/prowler:kustomized
command: ["/bin/sh", "-c"]
args:
- kubernetes
- --compliance
- cis_1.11_kubernetes
- --mutelist-file
- /tmp/mutelist/mutelist.yaml
- -z
- --output-formats
- html
- csv
- json-ocsf
- --output-directory
- /reports/prowler
- |
DATEDIR=/reports/prowler/$(date +%Y-%m-%d)
mkdir -p "$DATEDIR"
prowler kubernetes \
--compliance cis_1.11_kubernetes \
--mutelist-file /tmp/mutelist/mutelist.yaml \
-z \
--output-formats html csv json-ocsf \
--output-directory "$DATEDIR"
volumeMounts:
- name: reports
mountPath: /reports

View file

@ -1,5 +1,4 @@
# Minikube apiserver — flags managed by static pod manifests.
# Compensating control: cluster not internet-exposed; access via Tailscale ACLs.
Mutelist:
Accounts:
"*":
@ -7,48 +6,48 @@ Mutelist:
"apiserver_always_pull_images_plugin":
Regions: ["*"]
Resources: ["^kube-apiserver-minikube$"]
Description: "Minikube default; AlwaysPullImages not enabled."
Description: "CC: single-user-cluster, local-registry. Only the operator has cluster access; all images pulled from private zot registry."
"apiserver_audit_log_maxage_set":
Regions: ["*"]
Resources: ["^kube-apiserver-minikube$"]
Description: "Minikube does not configure audit logging."
Description: "CC: observability-stack-audit. Alloy/Loki provides pod-level audit trail."
"apiserver_audit_log_maxbackup_set":
Regions: ["*"]
Resources: ["^kube-apiserver-minikube$"]
Description: "Minikube does not configure audit logging."
Description: "CC: observability-stack-audit. Alloy/Loki provides pod-level audit trail."
"apiserver_audit_log_maxsize_set":
Regions: ["*"]
Resources: ["^kube-apiserver-minikube$"]
Description: "Minikube does not configure audit logging."
Description: "CC: observability-stack-audit. Alloy/Loki provides pod-level audit trail."
"apiserver_audit_log_path_set":
Regions: ["*"]
Resources: ["^kube-apiserver-minikube$"]
Description: "Minikube does not configure audit logging."
Description: "CC: observability-stack-audit. Alloy/Loki provides pod-level audit trail."
"apiserver_deny_service_external_ips":
Regions: ["*"]
Resources: ["^kube-apiserver-minikube$"]
Description: "Minikube default; no external IPs in use."
Description: "CC: tailscale-network-isolation. No external IPs routable; cluster only reachable via tailnet."
"apiserver_disable_profiling":
Regions: ["*"]
Resources: ["^kube-apiserver-minikube$"]
Description: "Minikube default; profiling endpoint not exposed."
Description: "CC: tailscale-network-isolation. Profiling endpoint unreachable from public internet."
"apiserver_encryption_provider_config_set":
Regions: ["*"]
Resources: ["^kube-apiserver-minikube$"]
Description: "Minikube does not configure etcd encryption at rest."
Description: "CC: tailscale-network-isolation, single-user-cluster. Etcd not network-exposed; only operator has node access."
"apiserver_kubelet_cert_auth":
Regions: ["*"]
Resources: ["^kube-apiserver-minikube$"]
Description: "Minikube manages kubelet certificates automatically."
Description: "CC: tailscale-network-isolation. Kubelet API not exposed outside the node; minikube auto-generates certificates."
"apiserver_request_timeout_set":
Regions: ["*"]
Resources: ["^kube-apiserver-minikube$"]
Description: "Minikube default; using K8s default timeout."
Description: "CC: tailscale-network-isolation. API server only reachable via tailnet; DoS risk limited to trusted clients."
"apiserver_service_account_lookup_true":
Regions: ["*"]
Resources: ["^kube-apiserver-minikube$"]
Description: "Minikube default."
Description: "CC: single-user-cluster. Only operator manages service accounts; no revoked tokens in circulation."
"apiserver_strong_ciphers_only":
Regions: ["*"]
Resources: ["^kube-apiserver-minikube$"]
Description: "Minikube default TLS cipher suite."
Description: "CC: tailscale-network-isolation. API server traffic encrypted by WireGuard at the network layer."

View file

@ -1,5 +1,4 @@
# Minikube control-plane components — managed by static pod manifests.
# Compensating control: cluster not internet-exposed; access via Tailscale ACLs.
Mutelist:
Accounts:
"*":
@ -7,12 +6,12 @@ Mutelist:
"controllermanager_disable_profiling":
Regions: ["*"]
Resources: ["^kube-controller-manager-minikube$"]
Description: "Minikube default; profiling endpoint not exposed outside tailnet."
Description: "CC: tailscale-network-isolation. Profiling endpoint unreachable from public internet."
"scheduler_profiling":
Regions: ["*"]
Resources: ["^kube-scheduler-minikube$"]
Description: "Minikube default; profiling endpoint not exposed outside tailnet."
Description: "CC: tailscale-network-isolation. Profiling endpoint unreachable from public internet."
"kubelet_tls_cert_and_key":
Regions: ["*"]
Resources: ["^kubelet-config$"]
Description: "Minikube uses auto-generated kubelet certificates."
Description: "CC: tailscale-network-isolation, single-user-cluster. Kubelet API not exposed outside node; minikube auto-generates certificates."

View file

@ -7,7 +7,7 @@ Mutelist:
"core_minimize_hostNetwork_containers":
Regions: ["*"]
Resources:
# Minikube control plane — requires hostNetwork by design
# Minikube control plane
- "^etcd-minikube$"
- "^kube-apiserver-minikube$"
- "^kube-controller-manager-minikube$"
@ -17,8 +17,9 @@ Mutelist:
- "^kindnet-"
- "^storage-provisioner$"
Description: >-
Control-plane and networking pods require hostNetwork.
All managed by minikube.
CC: tailscale-network-isolation. Control-plane and networking
pods require hostNetwork by design. Host network itself is
only reachable via tailnet.
"core_minimize_privileged_containers":
Regions: ["*"]
Resources:
@ -27,12 +28,13 @@ Mutelist:
# Tailscale operator-managed proxies
- "^ts-"
- "^ingress-"
# Forgejo runner — Docker-in-Docker for CI builds
# Forgejo runner
- "^forgejo-runner-"
Description: >-
kube-proxy: iptables (minikube). ts-*/ingress-*: network
namespace manipulation (Tailscale operator). forgejo-runner:
Docker-in-Docker for CI.
CC: single-user-cluster, operator-managed-pods, trusted-ci-only.
kube-proxy: system pod, single-user cluster. ts-*/ingress-*:
Tailscale operator-managed. forgejo-runner: DinD limited to
trusted private forge repos.
"core_seccomp_profile_docker_default":
Regions: ["*"]
Resources:
@ -47,34 +49,38 @@ Mutelist:
- "^nameserver-"
- "^ingress-"
Description: >-
System pods (minikube) and Tailscale operator pods — seccomp
profiles set by upstream/operator, not user manifests.
CC: single-user-cluster, operator-managed-pods. System pods
managed by minikube and Tailscale operator; seccomp profiles
set by upstream. Single-user cluster limits exploit surface.
"core_minimize_hostPID_containers":
Regions: ["*"]
Resources:
- "^prowler-"
Description: >-
Prowler CIS scanner requires hostPID to check file
permissions on kubelet and etcd data directories.
CC: ephemeral-privileged-jobs. Prowler CIS scanner requires
hostPID for file permission checks. Runs as CronJob with
7-day TTL, not a persistent workload.
"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.
CC: init-container-isolation. Root limited to init-chown-data
container; all runtime containers run as UID 472 with caps
dropped.
"core_minimize_containers_added_capabilities":
Regions: ["*"]
Resources:
# Minikube system pods
- "^coredns-"
- "^kindnet-"
# Grafana init-chown-data (CHOWN capability)
# Grafana init-chown-data
- "^grafana-"
Description: >-
System pods: NET_BIND_SERVICE/NET_RAW required by function
(minikube). Grafana: CHOWN for PVC init; all other
containers drop ALL.
CC: single-user-cluster, init-container-isolation. System
pods: capabilities required by function (minikube-managed).
Grafana: CHOWN limited to init phase; runtime containers
drop ALL.
"core_minimize_containers_capabilities_assigned":
Regions: ["*"]
Resources:
@ -82,5 +88,5 @@ Mutelist:
- "^kindnet-"
- "^grafana-"
Description: >-
System pods (minikube) and Grafana init-chown-data.
See core_minimize_containers_added_capabilities.
CC: single-user-cluster, init-container-isolation. See
core_minimize_containers_added_capabilities.

View file

@ -10,12 +10,12 @@ Mutelist:
# Built-in Kubernetes roles
- "^cluster-admin$"
- "^system:"
# ArgoCD — requires broad access for deployment management;
# ArgoCD itself is SSO-gated via Authentik
# ArgoCD
- "^argocd-"
Description: >-
Built-in K8s roles and ArgoCD. ArgoCD access is SSO-gated
via Authentik.
CC: single-user-cluster, sso-gated-admin-tools. Built-in
K8s roles: only operator can bind them. ArgoCD: requires
broad access but is SSO-gated via Authentik OIDC.
"rbac_minimize_pod_creation_access":
Regions: ["*"]
Resources:
@ -26,12 +26,14 @@ Mutelist:
# CloudNativePG operator
- "^cnpg-manager$"
Description: >-
Built-in K8s roles required for workload controllers.
cnpg-manager: CloudNativePG operator manages PostgreSQL pods.
CC: single-user-cluster. Built-in K8s roles and CNPG
operator. Only the operator can assign these roles; no
untrusted users have cluster access.
"rbac_minimize_service_account_token_creation":
Regions: ["*"]
Resources:
- "^system:"
Description: >-
kube-controller-manager requires token creation for service
account management. Built-in role.
CC: single-user-cluster. kube-controller-manager requires
token creation for SA management. Only operator manages
service accounts.