Deploy Prowler CIS scanner (#310)
All checks were successful
Build Container / detect (push) Successful in 4s
Build Container / build-dockerfile (prowler) (push) Successful in 10s

## Summary
- Deploy Prowler 5 as a weekly CronJob on minikube-indri for CIS Kubernetes Benchmark v1.11 scanning
- Custom slim container build (strips PowerShell, Trivy, and non-K8s providers from upstream)
- Reports (HTML, CSV, JSON-OCSF) written to NFS share on sifaka at `/volume1/reports/prowler/`
- Read-only ClusterRole for pod, RBAC, and control plane inspection
- Host path mounts + hostPID for kubelet file permission checks

## Follow-ups
- Mirror prowler-cloud/prowler on forge for supply chain control
- Build and push container image, update kustomization.yaml newTag
- Consider adding k3s-ringtail scanning (core + RBAC checks only)

## Test plan
- [ ] Build container: `mise run container-release prowler v5.22.0`
- [ ] Update `argocd/manifests/prowler/kustomization.yaml` newTag to built image tag
- [ ] Sync ArgoCD: `argocd app sync apps && argocd app set prowler --revision deploy-prowler && argocd app sync prowler`
- [ ] Trigger manual job: `kubectl create job --from=cronjob/prowler prowler-manual -n prowler --context=minikube-indri`
- [ ] Verify reports appear on sifaka NFS share
- [ ] `mise run services-check`

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

Reviewed-on: #310
This commit is contained in:
Erich Blume 2026-03-24 16:08:09 -07:00
commit d021b3534f
16 changed files with 449 additions and 25 deletions

17
argocd/apps/prowler.yaml Normal file
View file

@ -0,0 +1,17 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: prowler
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/prowler
destination:
server: https://kubernetes.default.svc
namespace: prowler
syncPolicy:
syncOptions:
- CreateNamespace=true

View file

@ -0,0 +1,55 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: prowler
namespace: prowler
spec:
schedule: "0 3 * * 0" # Sunday 3am
concurrencyPolicy: Forbid
jobTemplate:
spec:
ttlSecondsAfterFinished: 604800 # Auto-delete after 7 days
template:
spec:
serviceAccountName: prowler
containers:
- name: prowler
image: registry.ops.eblu.me/blumeops/prowler:kustomized
args:
- kubernetes
- --compliance
- cis_1.11_kubernetes
- -z
- --output-formats
- html
- csv
- json-ocsf
- --output-directory
- /reports/prowler
volumeMounts:
- name: reports
mountPath: /reports
- name: var-lib-kubelet
mountPath: /var/lib/kubelet
readOnly: true
- name: etc-kubernetes
mountPath: /etc/kubernetes
readOnly: true
- name: var-lib-etcd
mountPath: /var/lib/etcd
readOnly: true
hostPID: true
restartPolicy: OnFailure
volumes:
- name: reports
persistentVolumeClaim:
claimName: prowler-reports
- name: var-lib-kubelet
hostPath:
path: /var/lib/kubelet
- name: etc-kubernetes
hostPath:
path: /etc/kubernetes
- name: var-lib-etcd
hostPath:
path: /var/lib/etcd

View file

@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prowler
resources:
- serviceaccount.yaml
- rbac.yaml
- pv-nfs.yaml
- pvc.yaml
- cronjob.yaml
images:
- name: registry.ops.eblu.me/blumeops/prowler
newTag: v5.22.0-870be4e

View file

@ -0,0 +1,22 @@
# NFS PersistentVolume for Prowler compliance reports
# Requires: NFS share on sifaka at /volume1/reports with NFS permissions for indri
#
# To create on Synology:
# 1. Control Panel > Shared Folder > Create
# 2. Name: reports, Location: Volume 1
# 3. Control Panel > File Services > NFS > NFS Rules
# 4. Add rule for "reports" share: Hostname=indri, Privilege=Read/Write, Squash=No mapping
apiVersion: v1
kind: PersistentVolume
metadata:
name: prowler-reports-nfs-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
nfs:
server: sifaka
path: /volume1/reports

View file

@ -0,0 +1,13 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: prowler-reports
namespace: prowler
spec:
accessModes:
- ReadWriteMany
storageClassName: ""
volumeName: prowler-reports-nfs-pv
resources:
requests:
storage: 10Gi

View file

@ -0,0 +1,24 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: prowler-reader
rules:
- apiGroups: [""]
resources: ["pods", "configmaps", "nodes", "namespaces"]
verbs: ["get", "list", "watch"]
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: prowler-reader
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: prowler-reader
subjects:
- kind: ServiceAccount
name: prowler
namespace: prowler

View file

@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: prowler
namespace: prowler

View file

@ -0,0 +1,45 @@
# Prowler CIS scanner — slim build for Kubernetes provider only
# Strips PowerShell (M365), Trivy (IaC), and dashboard dependencies from upstream
ARG CONTAINER_APP_VERSION=5.22.0
FROM python:3.12-slim-bookworm AS build
ARG CONTAINER_APP_VERSION
RUN apt-get update && apt-get install -y --no-install-recommends \
git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \
https://forge.ops.eblu.me/mirrors/prowler.git .
# Install prowler into a virtualenv so we can copy it cleanly
RUN python -m venv /opt/prowler \
&& /opt/prowler/bin/pip install --no-cache-dir --upgrade pip \
&& /opt/prowler/bin/pip install --no-cache-dir .
# ---
FROM python:3.12-slim-bookworm
ARG CONTAINER_APP_VERSION
LABEL org.opencontainers.image.title="prowler"
LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}"
LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops"
LABEL org.opencontainers.image.vendor="blumeops"
LABEL org.opencontainers.image.description="Prowler CIS scanner (Kubernetes provider)"
RUN addgroup --gid 1000 prowler \
&& adduser --uid 1000 --gid 1000 --disabled-password --gecos "" prowler
COPY --from=build /opt/prowler /opt/prowler
ENV PATH="/opt/prowler/bin:${PATH}"
USER prowler
WORKDIR /home/prowler
ENTRYPOINT ["prowler"]

View file

@ -0,0 +1 @@
Deploy Prowler CIS scanner as a weekly CronJob on minikube-indri, with reports written to sifaka NFS share.

View file

@ -0,0 +1,61 @@
---
title: Deploy Prowler CIS Scanner
modified: 2026-03-24
last-reviewed: 2026-03-24
tags:
- how-to
- kubernetes
- security
- compliance
---
# Deploy Prowler CIS Scanner
Prowler runs weekly CIS Kubernetes Benchmark scans against minikube-indri and writes HTML/CSV/JSON reports to the NFS share on sifaka.
## What it checks
Prowler's Kubernetes provider runs ~70 checks from the CIS Kubernetes Benchmark v1.11, grouped into:
| Category | Checks | How it works |
|----------|--------|-------------|
| **Core (pod security)** | 13 | Queries K8s API for privileged containers, hostPID/hostNetwork, capabilities, secrets in env vars, seccomp |
| **RBAC** | 9 | Queries RBAC API for overprivileged roles, wildcard access, cluster-admin bindings |
| **Apiserver** | 29 | Inspects `kube-apiserver` pod args in kube-system (TLS, auth, audit, admission plugins) |
| **Etcd** | 7 | Inspects `etcd` pod args (TLS, cert auth) |
| **Controller Manager** | 7 | Inspects `kube-controller-manager` pod args |
| **Kubelet** | 16 | Reads kubelet-config ConfigMap + node file permissions (file checks need hostPID) |
| **Scheduler** | 2 | Inspects `kube-scheduler` pod args |
**Minikube relevance:** Most checks work because minikube runs control plane as static pods. Kubelet file permission checks return MANUAL unless Prowler runs on the node (we mount host paths to enable this).
**k3s note:** k3s embeds the control plane in a single binary — no static pods exist. Only core + RBAC checks (~22 of 70) produce results. Consider `kube-bench` for k3s control plane checks.
## Reports
Reports are written to `sifaka:/volume1/reports/prowler/` with timestamped filenames. See [[read-compliance-reports]] for how to access and interpret them.
## Running an ad-hoc scan
```fish
kubectl create job --from=cronjob/prowler prowler-manual -n prowler --context=minikube-indri
```
Watch progress:
```fish
kubectl logs -f job/prowler-manual -n prowler --context=minikube-indri
```
## Container
Custom slim build at `containers/prowler/Dockerfile` — strips PowerShell, Trivy, and non-Kubernetes providers from upstream. See [[build-container-image]] for the build/release process.
Source is mirrored at `forge.ops.eblu.me/mirrors/prowler`.
## See also
- [[security]] — security & compliance posture overview
- [[read-compliance-reports]] — how to access and interpret scan reports
- [[deploy-k8s-service]] — general K8s deployment how-to
- [[build-container-image]] — container build pipeline

View file

@ -0,0 +1,79 @@
---
title: Read Compliance Reports
modified: 2026-03-24
last-reviewed: 2026-03-24
tags:
- how-to
- security
- compliance
---
# Read Compliance Reports
How to access and interpret compliance scan reports from [[prowler]] and other security scanners.
## Accessing reports
Reports are stored on sifaka at `/volume1/reports/`. Each scanner writes to its own subdirectory:
| Scanner | Path | Schedule |
|---------|------|----------|
| [[prowler]] | `sifaka:/volume1/reports/prowler/` | Weekly (Sunday 3am) |
Copy reports to your local machine (remember `scp -O` for sifaka):
```fish
scp -O sifaka:/volume1/reports/prowler/prowler-output-In-Cluster-*.html /tmp/
open /tmp/prowler-output-In-Cluster-*.html
```
## Report formats
### HTML
Open in a browser. Self-contained, filterable by severity, status, and service. Best for human review — shows pass/fail per check with remediation guidance.
### CSV
One row per finding. Columns include check ID, status, severity, resource, namespace, description, and remediation. Good for filtering in a spreadsheet or scripting.
### JSON-OCSF
Open Cybersecurity Schema Framework format. Machine-parseable, suitable for SIEM ingestion or programmatic analysis.
### Compliance CSV
In the `compliance/` subdirectory. Findings mapped to specific framework requirement IDs (e.g., CIS 1.11 section numbers). Shows which controls pass or fail.
## Interpreting results
### Status values
- **PASS** — the resource is configured securely per the check
- **FAIL** — remediation is recommended
- **MANUAL** — Prowler cannot determine the result automatically (e.g., kubelet file permissions when not running on the node)
- **MUTED** — the finding was explicitly suppressed via a mutelist
### Severity
Findings are categorized as **critical**, **high**, **medium**, or **low**. Focus on critical and high first.
### Expected failures
Not all failures require action. Common expected failures in our minikube cluster:
- **Core/pod security (high):** System pods (ArgoCD, external-secrets, tailscale-operator) legitimately need elevated privileges. These can be mutelisted.
- **Apiserver (medium):** Audit logging, profiling, and some admission plugins are not configured in minikube defaults. Low risk for a homelab.
- **Kubelet (high):** Anonymous auth or read-only port settings from minikube defaults.
### Acting on findings
1. **Triage** — review new failures, distinguish real issues from expected noise
2. **Remediate** — fix what you can (pod security contexts, RBAC tightening)
3. **Mutelist** — suppress expected/accepted failures via Prowler's `--mutelist-file` to reduce noise in future scans
4. **Track** — compare reports over time to spot regressions
## See also
- [[security]] — security & compliance posture overview
- [[deploy-prowler]] — Prowler deployment and ad-hoc scans

View file

@ -40,6 +40,7 @@ Registry of all applications deployed via [[argocd]].
| `forgejo-runner` | forgejo-runner | `argocd/manifests/forgejo-runner/` | [[forgejo]] CI |
| `ollama` | ollama | `argocd/manifests/ollama/` | [[ollama]] |
| `mealie` | mealie | `argocd/manifests/mealie/` | [[mealie]] |
| `prowler` | prowler | `argocd/manifests/prowler/` | [[prowler]] |
## Sync Policies

View file

@ -0,0 +1,53 @@
---
title: Security & Compliance
modified: 2026-03-24
last-reviewed: 2026-03-24
tags:
- operations
- security
---
# Security & Compliance
Security posture and compliance scanning for BlumeOps infrastructure.
## Compliance frameworks
| Framework | Tool | Cluster | Notes |
|-----------|------|---------|-------|
| CIS Kubernetes Benchmark v1.11 | [[prowler]] | minikube-indri | Weekly CronJob, ~82 checks |
| PCI DSS v4.0 (K8s mapping) | [[prowler]] | minikube-indri | Reuses CIS checks mapped to PCI requirements |
| ISO 27001:2022 (K8s mapping) | [[prowler]] | minikube-indri | Partial — 22 of 92 controls mapped |
## Scanning tools
- [[prowler]] — CIS Kubernetes Benchmark scanner (weekly CronJob)
- [[deploy-prowler]] — deployment and ad-hoc scan how-to
- [[read-compliance-reports]] — accessing and interpreting reports
## Identity & access
- [[authentik]] — SSO/OIDC provider for all web services
- RBAC — Kubernetes role-based access control (audited by Prowler RBAC checks)
## Network & TLS
- [[caddy]] — TLS termination for `*.ops.eblu.me` services
- [[flyio-proxy]] — public ingress via Fly.io tunnel
- Tailscale — zero-trust mesh networking across all nodes
## Secrets management
- [[1password]] — root credential store
- [[external-secrets]] — Kubernetes secrets synced from 1Password
## Reports
All compliance scan reports are stored on `sifaka:/volume1/reports/`. See [[read-compliance-reports]] for access and interpretation.
## Known gaps
- No SOC 2 compliance mapping for Kubernetes (Prowler only maps SOC 2 for AWS/Azure/GCP)
- k3s control plane checks produce no results (embedded binary, no static pods) — consider kube-bench
- No container image vulnerability scanning yet (Prowler has an `image` provider)
- No IaC scanning of manifests/Dockerfiles yet (Prowler has an `iac` provider using Trivy)

View file

@ -0,0 +1,32 @@
---
title: Prowler
modified: 2026-03-24
last-reviewed: 2026-03-24
tags:
- service
- security
---
# Prowler
CIS Kubernetes Benchmark scanner for compliance posture reporting.
## Quick Reference
| Property | Value |
|----------|-------|
| **Namespace** | `prowler` |
| **Image** | `registry.ops.eblu.me/blumeops/prowler` (see `argocd/manifests/prowler/kustomization.yaml` for current tag) |
| **Schedule** | Weekly (Sunday 3am) |
| **Reports** | `sifaka:/volume1/reports/prowler/` (NFS) |
| **Manifests** | `argocd/manifests/prowler/` |
## What it does
Runs Prowler 5 as a CronJob against minikube-indri, executing CIS Kubernetes Benchmark v1.11 checks across pod security, RBAC, apiserver, etcd, kubelet, controller-manager, and scheduler. Reports are written in HTML, CSV, and JSON-OCSF to the NFS share on sifaka.
## See also
- [[security]] — security & compliance posture overview
- [[deploy-prowler]] — deployment how-to, ad-hoc scan instructions, check relevance notes
- [[read-compliance-reports]] — how to access and interpret reports

View file

@ -7,10 +7,10 @@
#USAGE arg "<container>" help="Container name (directory under containers/)"
#USAGE flag "--ref <ref>" help="Commit SHA or branch to build (defaults to current HEAD)"
#USAGE flag "--dry-run" help="Show what would be done without triggering"
"""Trigger container build workflows via Forgejo API dispatch.
"""Trigger container build workflow via Forgejo API dispatch.
Dispatches both Build Container and Build Container (Nix) workflows.
Each workflow checks for its build file and skips if not present.
Dispatches the unified build-container workflow, which handles both
Dockerfile and Nix builds in a single workflow.
"""
import subprocess
@ -26,10 +26,7 @@ FORGE_API = f"{FORGE_URL}/api/v1"
REPO = "eblume/blumeops"
FORGE_ACTIONS = f"{FORGE_URL}/{REPO}/actions"
WORKFLOWS = [
"build-container.yaml",
"build-container-nix.yaml",
]
WORKFLOW = "build-container.yaml"
app = typer.Typer(add_completion=False)
@ -108,9 +105,7 @@ def main(
typer.echo()
if dry_run:
typer.echo("[dry-run] Would dispatch workflows:")
for wf in WORKFLOWS:
typer.echo(f" - {wf}")
typer.echo(f"[dry-run] Would dispatch {WORKFLOW}")
typer.echo()
typer.echo(f"Monitor builds at: {FORGE_ACTIONS}")
return
@ -121,8 +116,7 @@ def main(
"Content-Type": "application/json",
}
for wf in WORKFLOWS:
url = f"{FORGE_API}/repos/{REPO}/actions/workflows/{wf}/dispatches"
url = f"{FORGE_API}/repos/{REPO}/actions/workflows/{WORKFLOW}/dispatches"
payload = {
"ref": "main",
"inputs": {
@ -132,9 +126,9 @@ def main(
}
resp = httpx.post(url, json=payload, headers=headers, timeout=30)
if resp.status_code == 204:
typer.echo(f"Dispatched {wf}")
typer.echo(f"Dispatched {WORKFLOW}")
else:
typer.echo(f"Error dispatching {wf}: {resp.status_code} {resp.text}")
typer.echo(f"Error dispatching {WORKFLOW}: {resp.status_code} {resp.text}")
raise typer.Exit(1)
typer.echo()

View file

@ -271,6 +271,13 @@ services:
upstream-source: https://github.com/unpoller/unpoller/releases
notes: UniFi metrics exporter for Prometheus
- name: prowler
type: argocd
last-reviewed: 2026-03-24
current-version: "5.22.0"
upstream-source: https://github.com/prowler-cloud/prowler/releases
notes: CIS Kubernetes Benchmark scanner; weekly CronJob on minikube-indri
- name: forgejo
type: ansible
last-reviewed: 2026-02-22