diff --git a/argocd/apps/prowler.yaml b/argocd/apps/prowler.yaml new file mode 100644 index 0000000..a98aa4f --- /dev/null +++ b/argocd/apps/prowler.yaml @@ -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 diff --git a/argocd/manifests/prowler/cronjob.yaml b/argocd/manifests/prowler/cronjob.yaml new file mode 100644 index 0000000..bc00831 --- /dev/null +++ b/argocd/manifests/prowler/cronjob.yaml @@ -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 diff --git a/argocd/manifests/prowler/kustomization.yaml b/argocd/manifests/prowler/kustomization.yaml new file mode 100644 index 0000000..a8b9840 --- /dev/null +++ b/argocd/manifests/prowler/kustomization.yaml @@ -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 diff --git a/argocd/manifests/prowler/pv-nfs.yaml b/argocd/manifests/prowler/pv-nfs.yaml new file mode 100644 index 0000000..aa81405 --- /dev/null +++ b/argocd/manifests/prowler/pv-nfs.yaml @@ -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 diff --git a/argocd/manifests/prowler/pvc.yaml b/argocd/manifests/prowler/pvc.yaml new file mode 100644 index 0000000..8d94378 --- /dev/null +++ b/argocd/manifests/prowler/pvc.yaml @@ -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 diff --git a/argocd/manifests/prowler/rbac.yaml b/argocd/manifests/prowler/rbac.yaml new file mode 100644 index 0000000..38fcfae --- /dev/null +++ b/argocd/manifests/prowler/rbac.yaml @@ -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 diff --git a/argocd/manifests/prowler/serviceaccount.yaml b/argocd/manifests/prowler/serviceaccount.yaml new file mode 100644 index 0000000..26aaaa7 --- /dev/null +++ b/argocd/manifests/prowler/serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: prowler + namespace: prowler diff --git a/containers/prowler/Dockerfile b/containers/prowler/Dockerfile new file mode 100644 index 0000000..cb557ca --- /dev/null +++ b/containers/prowler/Dockerfile @@ -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"] diff --git a/docs/changelog.d/deploy-prowler.feature.md b/docs/changelog.d/deploy-prowler.feature.md new file mode 100644 index 0000000..64236c7 --- /dev/null +++ b/docs/changelog.d/deploy-prowler.feature.md @@ -0,0 +1 @@ +Deploy Prowler CIS scanner as a weekly CronJob on minikube-indri, with reports written to sifaka NFS share. diff --git a/docs/how-to/operations/deploy-prowler.md b/docs/how-to/operations/deploy-prowler.md new file mode 100644 index 0000000..2b29d39 --- /dev/null +++ b/docs/how-to/operations/deploy-prowler.md @@ -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 diff --git a/docs/how-to/operations/read-compliance-reports.md b/docs/how-to/operations/read-compliance-reports.md new file mode 100644 index 0000000..e2088f7 --- /dev/null +++ b/docs/how-to/operations/read-compliance-reports.md @@ -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 diff --git a/docs/reference/kubernetes/apps.md b/docs/reference/kubernetes/apps.md index 270cc55..02215fc 100644 --- a/docs/reference/kubernetes/apps.md +++ b/docs/reference/kubernetes/apps.md @@ -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 diff --git a/docs/reference/operations/security.md b/docs/reference/operations/security.md new file mode 100644 index 0000000..8a621b1 --- /dev/null +++ b/docs/reference/operations/security.md @@ -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) diff --git a/docs/reference/services/prowler.md b/docs/reference/services/prowler.md new file mode 100644 index 0000000..f68a573 --- /dev/null +++ b/docs/reference/services/prowler.md @@ -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 diff --git a/mise-tasks/container-build-and-release b/mise-tasks/container-build-and-release index ce57c2e..508d586 100755 --- a/mise-tasks/container-build-and-release +++ b/mise-tasks/container-build-and-release @@ -7,10 +7,10 @@ #USAGE arg "" help="Container name (directory under containers/)" #USAGE flag "--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,21 +116,20 @@ def main( "Content-Type": "application/json", } - for wf in WORKFLOWS: - url = f"{FORGE_API}/repos/{REPO}/actions/workflows/{wf}/dispatches" - payload = { - "ref": "main", - "inputs": { - "container": container, - "ref": ref, - }, - } - resp = httpx.post(url, json=payload, headers=headers, timeout=30) - if resp.status_code == 204: - typer.echo(f"Dispatched {wf}") - else: - typer.echo(f"Error dispatching {wf}: {resp.status_code} {resp.text}") - raise typer.Exit(1) + url = f"{FORGE_API}/repos/{REPO}/actions/workflows/{WORKFLOW}/dispatches" + payload = { + "ref": "main", + "inputs": { + "container": container, + "ref": ref, + }, + } + resp = httpx.post(url, json=payload, headers=headers, timeout=30) + if resp.status_code == 204: + typer.echo(f"Dispatched {WORKFLOW}") + else: + typer.echo(f"Error dispatching {WORKFLOW}: {resp.status_code} {resp.text}") + raise typer.Exit(1) typer.echo() typer.echo(f"Monitor builds at: {FORGE_ACTIONS}") diff --git a/service-versions.yaml b/service-versions.yaml index 2aa82fd..321efe8 100644 --- a/service-versions.yaml +++ b/service-versions.yaml @@ -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