# GRC Open-Source Tools: Falco, Wazuh & Probo (CrowdStrike/Vanta Alternative, Zero SaaS Bills)

*Part of the homelab build series. Governance, risk and compliance on open source only: Kyverno blocks, Falco detects live threats, Wazuh correlates and maps them to MITRE ATT&CK and SOC 2, and Probo holds the evidence. A brownfield build on an RKE2 cluster that already runs Istio, Keycloak, Ceph, Argo CD, Kyverno, KubeArmor and Trivy. Highly available: three Wazuh indexers, one per Proxmox host, and three Wazuh managers.*

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/480442fe-28c6-485c-aba4-f3bd4b9325c7.png align="center")

---

> Open-source GRC: Kyverno, Falco, Wazuh and Probo

## Introduction

My cluster had 41 Kyverno policies, KubeArmor enforcing AppArmor profiles and Trivy scanning every image. If someone had opened a shell inside one of my containers, none of it would have told me.

That gap is what companies pay CrowdStrike to close. They pay Vanta to turn it into audit evidence, and Azure Policy to govern the whole thing, as long as it lives in Azure. All three are good products, and all three keep your data on their platform and bill you every year to keep it there.

I wanted the same capability on hardware I already own, with every rule and mapping in Git. So I added three open-source tools to the platform. Falco watches every system call on the nodes. Wazuh collects those alerts next to the host logs and tags each one with a MITRE technique and a SOC 2 criterion. Probo tracks the SOC 2 controls and which part of the platform answers each of them. Kyverno, already running, stays the gate at admission.

It works. A shell opened in a container now reaches the SIEM in under a second, posts to Slack and maps to SOC 2 control CC7.2. Getting there took more tuning than building. The first hour produced about 18,500 alerts, and I had to bring that down to 176 without hiding a single real attack.

To be clear about what this is: it replaces the tools, not the people behind a managed service. I'll come back to what that costs you near the end.

## What you are locked into

With CrowdStrike Falcon you pay per device, billed annually and up front, so every node you add raises the bill. Your telemetry lives in their cloud. You can write your own detections, but you can't read theirs, and the threat intelligence stops the day you cancel.

Vanta doesn't publish prices. Buyers say the quote grows with headcount and with every framework you add. Over time your control mappings, your evidence and your audit history all build up inside it, collected through its integrations, so leaving means rebuilding evidence collection somewhere else.

Azure Policy governs Azure. A cluster outside Azure can join through Azure Arc, which installs Azure Policy as an extension running Gatekeeper, but Azure's control plane stays in the loop and the policies are written in Azure's JSON format with Azure resource aliases. They don't come with you.

In this build every rule is Falco YAML, every mapping is Wazuh XML and every control is a line in a Git file. If I moved clusters tomorrow, all of it would come along.

## Architecture

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/0b3128b2-f800-4af9-a404-95a1087cfe94.png align="center")

> Architecture: Falco on each node, Wazuh agents on the hosts, the Istio gateway, the Wazuh cluster, Slack and Probo

I think of it as four layers, each answering one question:

| Layer | Tool | Question | Stands in for |
| --- | --- | --- | --- |
| Prevent | Kyverno | May this run at all? | Azure Policy |
| Detect | Falco | What is running right now, and is it an attack? | CrowdStrike |
| Correlate | Wazuh | What does it mean, and which technique and criterion does it hit? | the SIEM |
| Attest | Probo | Which control does it satisfy, and where's the evidence? | Vanta |

Kyverno came earlier in this series, so I don't rebuild it here. Its policy reports don't reach Wazuh yet, which is why that arrow is dashed.

## Where the agents run

Wazuh documents two ways to run its agent in Kubernetes, as a DaemonSet or as a sidecar. I didn't use either. Ansible installs the agent on the host itself, on all 20 RKE2 VMs and the 3 Proxmox hosts.

The reasoning is simple: whatever watches the cluster shouldn't fall over when the cluster does. A DaemonSet agent only sees its own container unless you give it the host filesystem, privileged mode and the host PID namespace, at which point your security agent is the most privileged pod on every node. If the cluster goes down, a DaemonSet agent goes with it, and a dead node just looks like silence. The documented DaemonSet also mounts the Docker socket, and RKE2 runs containerd. And the Proxmox hosts aren't Kubernetes nodes at all, so only a host install reaches them.

So Falco covers the containers, and the host agent covers the host and carries Falco's alerts out.

## Deploying Wazuh into a brownfield cluster

I vendored Wazuh's Kubernetes manifests at v4.14.9 without touching them, and change them only through patches in a Kustomize overlay that Argo CD applies. Upstream assumes a cloud and an empty cluster. Mine is neither, and several things broke.

The first surprise was four `type: LoadBalancer` Services. With MetalLB in the cluster, the dashboard, the manager API, the workers and the indexer API on 9200 would each have taken an address on my LAN. All four became ClusterIP, and only the Istio gateway is exposed. The manifests also ship default credentials, `admin:SecretPassword` and an enrolment password of `password`, plus four demo users with known hashes. Terraform generates every certificate, password and hash into Vault instead, and the Vault Secrets Operator syncs them into Secrets with the names upstream expects. Upstream's secret files never make it into the overlay.

Then the service mesh broke Wazuh's own clustering. With Istio sidecars, every connection reached the master from `127.0.0.6`, so the two workers looked like one, only two of the three indexers joined, and Filebeat failed TLS. Wazuh already encrypts every hop, so I took the namespace out of the mesh. Upstream also sets `vm.max_map_count` from a privileged init container. Ansible sets it on the database nodes now, the init container is gone, and the namespace enforces Pod Security baseline. Restricted isn't possible, because the manager runs as root.

The one that cost me most was disk. Each manager downloads a 9.2 GB vulnerability feed, and upstream gives it 500Mi. I'd raised that to 10Gi, and within 15 minutes all three volumes were full, `wazuh-db` couldn't open its database, and the master stopped taking events. They have 40Gi each on Ceph now, on `ceph-block` instead of upstream's cloud StorageClass.

Agents reach the managers through the same Istio ingress gateway as everything else, with ports 1514 (events) and 1515 (enrolment) added as TCP routes. On the first enrolment, agents registered with the master and then connected to a worker that didn't have their key yet. They got "Invalid ID", tried again, were refused as duplicates, and settled after a few minutes.

At the end of it I had 23 agents active, 3 indexers green, a master and 2 workers.

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/4de13946-2aa3-41ea-8d2b-f57d520f7baf.png align="center")

> Wazuh Endpoints: 23 agents active, 20 Ubuntu RKE2 VMs and 3 Debian Proxmox hosts, all on Wazuh v4.14.9

## Single sign-on with Keycloak

The dashboard signs in through Keycloak, like every other app on the platform. Terraform creates a `wazuh` realm with Google and GitHub sign-in and an email allow-list, so any other account is turned away at first login. The indexer maps the allowed emails to `all_access`, and the internal admin from Vault stays as a break-glass login.

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/a60460ff-0cb7-4b86-ae12-a4edbfa7be40.png align="center")

> The Wazuh login page: the internal login stays as a break-glass, and "Sign in with Keycloak" is how people get in

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/f2ce50ad-82de-45fb-b508-0f569e273dd1.png align="center")

> Keycloak realm wazuh: sign in with Google or GitHub; only allow-listed emails get through

## Bringing Falco back

Falco had been switched off on this cluster for a while. It was using 875 to 956 millicores per node against a 100m request, and I needed the CPU.

When I turned it back on, every pod crash-looped. Falco itself started fine, but its container plugin (0.6.3, bundled with chart 8.0.2) panicked on a nil pointer: containerd 2.3 leaves a field empty that the plugin reads without checking. Pinning the plugin to 0.7.4 fixed it without a chart upgrade.

```yaml
collectors:
  containerEngine:
    pluginRef: ghcr.io/falcosecurity/plugins/plugin/container:0.7.4
```

A changed ConfigMap doesn't restart a DaemonSet, so I had to restart the pods before they picked it up.

## Routing Falco alerts to Wazuh

My first idea was Falcosidekick posting straight to Wazuh. That doesn't work, because the manager listens for syslog, not HTTP, and every alert would arrive with no idea which host it came from.

What I did instead needs no new component. Falco writes each alert as one JSON line to a file on the node, and the Wazuh agent that's already on that host reads it. Every alert reaches Wazuh tagged with the right host, through the same gateway and TLS as everything else.

```yaml
falco:
  file_output:
    enabled: true
    keep_alive: false
    filename: /var/log/falco/events.json

mounts:
  volumes:
    - name: falco-events
      hostPath:
        path: /var/log/falco
        type: DirectoryOrCreate
  volumeMounts:
    - name: falco-events
      mountPath: /var/log/falco
```

I didn't edit 23 hosts to point the agents at that file. Wazuh's shared configuration does it from the master:

```xml
<agent_config>
  <localfile>
    <log_format>json</log_format>
    <location>/var/log/falco/events.json</location>
  </localfile>
</agent_config>
```

A small set of rules, IDs 100300 to 100399, turns Falco's priorities into Wazuh levels and tags each alert with a MITRE technique and a SOC 2 criterion:

```xml
<rule id="100311" level="8">
  <if_sid>100302</if_sid>
  <field name="rule">^Read sensitive file untrusted$|^Read Sensitive File in Container$</field>
  <description>Falco: sensitive file read in $(output_fields.k8s.ns.name)/$(output_fields.k8s.pod.name)</description>
  <mitre>
    <id>T1003.008</id>
  </mitre>
  <group>tsc_CC7.2,tsc_CC7.3,</group>
</rule>
```

That `keep_alive: false` in the Falco config looks like a detail. It isn't, and the tuning section explains why. Two other things caught me out here. The master syncs new rules to the workers, but the workers keep using the old ones until `analysisd` reloads (`PUT /cluster/analysisd/reload`), so for a while half the cluster looked silent. And the file needs rotating, which logrotate does with `copytruncate`, rolled out by Ansible. The agent notices the truncation and reads from the top again.

## Alerts in Slack

Anything at level 8 or above goes to Slack, into the channel Alertmanager already posts to. Wazuh has a built-in Slack integration, so the message carries the mapped alert: host, pod, MITRE technique and SOC 2 criterion.

The webhook is a secret, and `ossec.conf` can't read secrets from a file. So the Vault Secrets Operator renders the whole integration block, and an init container appends it to the manager's config when the pod starts:

```yaml
transformation:
  excludeRaw: true
  templates:
    slack.conf:
      text: |-
        <ossec_config>
          <integration>
            <name>slack</name>
            <hook_url>{{ get .Secrets "hook_url" | trim }}</hook_url>
            <level>8</level>
            <alert_format>json</alert_format>
          </integration>
        </ossec_config>
```

Terraform fills that Vault value from the same variable as Alertmanager's webhook, so rotating one rotates both. While setting this up I also worked out why I'd once missed an Alertmanager alert. A Slack incoming webhook posts to the channel it was created for and ignores the `channel` in your config. My alerts had been arriving all along, just not in the channel the config named, and the one I needed was buried among routine warnings.

Something to know before you copy this: Wazuh writes the full webhook URL to `/var/ossec/logs/integrations.log` every time it sends. Don't screenshot that file, and assume anyone who can exec into the managers has the URL.

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/6c7e83ce-f321-43da-bfe8-73bd396edc69.png align="center")

> #monitoring-stack-alert: the shell test as Wazuh rule 100310, level 8, with agent and location. Below it, one of the manager start-up false alarms that the wazuh-syscheckd exemption later removed

## Validation

Nothing here runs on a schedule. Falco looks at every system call as it happens. To check the whole chain, I ran three real attacks against a pod that was already in the cluster:

```bash
# Read the password hashes
kubectl -n probo exec deploy/probo-chrome -- cat /etc/shadow

# An interactive shell inside a container
script -q /dev/null kubectl -n probo exec -it deploy/probo-chrome -- sh

# Tamper with the container's configuration
kubectl -n probo exec deploy/probo-chrome -- sh -c 'echo t > /etc/falco-write-test; rm -f /etc/falco-write-test'
```

All three arrived in Wazuh with the right host, pod and namespace, and so did a real privileged container launch:

| Action | Falco rule | Wazuh rule | Level | MITRE |
| --- | --- | --- | --- | --- |
| `cat /etc/shadow` | Read sensitive file untrusted | 100311 | 8 | T1003.008 |
| `kubectl exec -it … sh` | Terminal shell in container | 100310 | 8 | T1059 |
| Write below `/etc` | Write to /etc in Container | 100302 | 5 | none |
| Privileged pod replaced | Launch Privileged Container in Homelab | 100322 | 3 | T1611 |

From Falco seeing the system call to Wazuh storing the alert took 0.72 seconds.

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/bab2b31c-1921-4313-ab2d-100f8e7550e6.png align="center")

> Wazuh Discover, filtered to Falco alerts from the probo namespace: the shadow read (100311, T1003.008), the interactive shell (100310, T1059) and the /etc write (100302), each tagged with the host agent that shipped it

## Tuning the noise

The first hour was about 18,500 Falco alerts, nearly all of them the platform going about its normal business. Nobody can read that, so most of this project ended up being tuning.

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/40493827-7482-4508-b8a2-64f9ad82c3f7.png align="center")

> Falco alerts per hour before and after tuning, and the 332 s to 0.72 s delay fix

I used the same loop every time. Find the top image and namespace for each rule in Wazuh, decide whether it's a threat or a known workload, fix it in Git, then compare the hour before with the hour after.

The biggest single fix was embarrassing. Our own "Read Sensitive File in Container" rule listed `/etc/passwd`, which fired about 3,600 times an hour as MySQL, Postgres, Ceph and Trino looked up user names. That file is world-readable on purpose. I took it out and put `/etc/gshadow` in. Close behind it was Wazuh's own rootcheck reading `/etc/shadow` on every host, which is the security agent tripping the security rule. I exempted it by its executable path, `/var/ossec/bin/wazuh-modulesd`, rather than a process name anything could copy. Later the managers' file-integrity daemon needed the same treatment, after every manager restart posted three false alarms to Slack.

Two rules were broken in ways I'd never have guessed. "Launch Privileged Container" had no event type in its condition, so it matched every system call of a privileged container that was already running, about 3,600 times an hour from one CSI driver. Falco even warns about this at load time (`LOAD_NO_EVTTYPE`). It now matches only a container's first process, and deleting one of those CSI pods produced exactly one alert. The kubectl rule excluded `(ci_cd_images, kubectl_images)`, but neither list was defined anywhere, and Falco quietly reads an undefined name as a plain string. That exclusion had never matched anything, which is why the kube-apiserver's own health probe was firing thousands of times an hour.

For everything else I exempted by namespace and image together. Controllers that talk to the Kubernetes API by design, such as Tekton, Kiali, Kyverno and Argo CD, are allowed to only in their own namespace with their own image. KubeArmor, the Prometheus config reloader and Istio's `pilot-agent` may write their own config below `/etc`, but only in their own directory. A reverse-shell rule that matched `-i` anywhere in a bash command had to be narrowed too, because Rook-Ceph's health probes run `curl -i` and produced over a thousand false Critical alerts in three hours. The last one turned up while I was writing this. Our GitHub Actions self-hosted runners download `kubectl`, `crane` and `yq` during a job and run them, which is exactly what Falco's "drop and execute a new binary" rule looks for. One CI run raised 594 Critical alerts, every one of which would have gone to Slack. The runners are now exempt for binaries under their own home directory, and only there.

After every change I ran the attack tests again, and they all still fired. That mattered more to me than the drop in numbers.

The same story is on a dashboard, kept in Git like everything else. A small generator writes the saved objects, and a Job imports them after every Argo CD sync.

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/f0748fda-b571-4c88-b249-7f8f04c6b84b.png align="center")

> Falco runtime security: every Falco alert carries SOC 2 CC7.2; the 2,607 at level 8 and above also carry CC7.3 and go to Slack

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/917b7357-0395-4ec5-b81f-ec8ba9428dd6.png align="center")

> The last 24 hours in half-hour bars: about 10,000 alerts every 30 minutes before tuning, then a trickle. The MITRE techniques sit beside them

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/dad6f434-710f-454d-8df8-10edeee27563.png align="center")

> Where the alerts came from, by namespace and image, and the level 8+ events with their MITRE technique

Getting rid of the noise exposed a problem I hadn't seen before. I wrote a test file below `/etc` and watched it appear on Falco's stdout straight away, then waited. It reached Wazuh 332 seconds later, and only because I restarted that Falco pod. With `keep_alive: true`, Falco keeps the output file open and buffers its writes, even with `buffered_outputs: false`. At 18,500 alerts an hour the buffer filled every few seconds, so I never noticed. At a few dozen an hour, a real alert could sit there for minutes. Switching to `keep_alive: false` makes Falco write each alert as it happens, and the same test took 0.72 seconds. If you tune a pipeline like this, measure the timing again afterwards.

There was a nice side effect, too. Before tuning, Falco averaged 294 millicores per node. After, the same 14 pods average 28.

## Compliance mapping in Wazuh

This is where the security work turns into compliance evidence. Wazuh's own ruleset tags its checks with PCI DSS, NIST 800-53, HIPAA, GDPR and SOC 2, so a failed check arrives already attached to named controls.

Here's a real one. Wazuh runs the CIS Ubuntu 24.04 benchmark on every host agent, and this check fails on all 20 of my RKE2 VMs:

| Field | Value |
| --- | --- |
| Check | CIS Ubuntu Linux 24.04 LTS 3.3.5: Ensure ICMP redirects are not accepted |
| Wazuh rule | 19007, level 7 |
| SOC 2 | CC6.3, CC6.6, CC7.1, CC7.2 |
| PCI DSS v4 | 1.2.5, 2.2.4, 6.4.1 |
| ISO 27001:2013 | A.13.1.3 |
| NIST 800-53 | CM.1 |
| MITRE ATT&CK | T1557, mitigations M1030 and M1042 |

That's as far as Wazuh goes. It produces the mapped technical evidence, but it doesn't track who owns a control or what an auditor has already seen. For that I use Probo.

## Evidence in Probo

Probo is an open-source compliance tool. It runs in the same cluster and signs in through Keycloak with SAML. Its frameworks come with their controls already written, and I added all 13 to my organisation, 1,008 controls in total:

| Framework | Controls | Measures linked |
| --- | --- | --- |
| SOC 2 | 61 | 11 |
| PCI DSS | 331 | not yet |
| ISO 27001 (2022) | 123 | not yet |
| ISO 27701 (2025) | 108 | not yet |
| ISO 42001 (2023) | 70 | not yet |
| DORA | 65 | not yet |
| GDPR | 62 | not yet |
| HIPAA | 60 | not yet |
| HDS | 31 | not yet |
| FERPA | 29 | not yet |
| 21 CFR Part 11 | 28 | not yet |
| CCPA | 23 | not yet |
| NIS 2 | 17 | not yet |

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/798fa392-75d0-428e-b851-20502590fcc9.png align="center")

Keycloak realm Probo: sign in with Google or GitHub; only allow-listed emails get through

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/5194e008-1377-409f-9057-8d9e337e3e0f.png align="center")

Created *George Homelab as organisation*

I started with SOC 2, because Wazuh already tags every alert with its criteria.

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/a5cf4e07-ebde-4b8e-a3f4-ac2b6529f108.png align="center")

> The 13 frameworks added to my Probo organisation; only SOC 2 has measures linked so far

In Probo, each control the platform really runs becomes a measure, linked to the criteria it answers. I didn't want to click those together in a UI, so they live in a YAML file in Git:

```yaml
- code: IC-MON-01
  name: Runtime threat detection (Falco → Wazuh)
  category: Monitoring
  control_type: DETECTIVE
  operating_frequency:
    mode: CONTINUOUS
  implementation_status: OPERATING
  soc2:
    - CC7.2
    - CC7.3
```

A short script, `probo-sync.py`, reads that file and talks to Probo's MCP API. It shows a plan first, only applies with `--apply`, and never deletes anything. The first run created 11 measures and 19 links, covering runtime detection, host assessment, sign-in, secrets, network segmentation, admission policy, GitOps change control, the image supply chain and tested backups. Where I know a measure has a gap, it says "in progress". When I add ISO 27001, I'll link the same measures to its controls. The evidence doesn't change, only the framework it's mapped to.

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/7c1e8ec5-7e62-4bfa-a474-655ef658c515.png align="center")

> SOC 2 control CC7.2 in Probo, answered by three measures: runtime threat detection and the Wazuh host assessment, both implemented, and Prometheus alerting, still in progress

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/8ff10158-e3dd-4c3f-a124-058d1cf89cc8.png align="center")

> All 11 measures in Probo, created from soc2-mapping.yaml: seven implemented, four still in progress where I know there's a gap

## One event, end to end

![](https://cdn.hashnode.com/uploads/covers/67014345dbb510bc35d60f47/943dc25e-29dd-40c7-a81f-ad66d1d588b7.png align="center")

> One shell in a container, from the system call to Slack and the SOC 2 control

Following one shell through the whole chain: Falco matches "Terminal shell in container" on the node where the pod runs. The Wazuh agent on that node reads the JSON line and sends it through the Istio gateway on port 1514. The manager applies rule 100310, which makes it level 8 and tags it T1059 and SOC 2 CC7.2 and CC7.3. The alert is stored 0.72 seconds after the system call and posted to Slack. In Probo, CC7.2 is answered by the runtime detection measure, which names this pipeline as its evidence.

The last step is the weak one. The measure shows the control exists and runs, but one alert doesn't flow into Probo by itself. That's what I'll build next.

## How reliable is it?

The lab powers off every night and cold-boots every morning, so every part of this restarts from scratch daily, which turns out to be a decent test in itself.

| What goes wrong | What happens | Evidence |
| --- | --- | --- |
| All three hosts cold-boot | Wazuh, Falco and Probo come back with no manual step | 23 agents active and 3 indexers green after the morning boot |
| A Wazuh manager restarts | Its agents reconnect to the managers still running | All three managers restarted one after another: all 23 agents reconnected, and every test alert sent during the restarts arrived |
| Falco restarts on every node (14 pods) | Each pod is replaced in turn and picks up where it stopped | Several rolling restarts during tuning; every attack test run afterwards fired |
| An indexer pod dies | The cluster drops to yellow, never red, and alerts keep landing. The pod is back in about 10 seconds | Green again 60 seconds after the delete; an attack test sent during the outage was stored 0.21 s after Falco saw it. The first run went red: Wazuh creates its inventory and vulnerability indices with 0 replicas. An hourly job now keeps one replica on them |
| One Proxmox host goes down | \[PENDING: shut down one host, check agents fail over to the remaining workers and the indexer keeps 2 of 3\] | \[PENDING\] |

## What this doesn't do

CrowdStrike sells more than a sensor. You also get managed threat hunting and threat intelligence drawn from their whole customer base. Falco and Wazuh give me the detection, but none of that service.

Detection is automatic here, and response isn't. When an alert lands in Slack, a person decides what happens next. Falco Talon could kill a pod on its own, but I haven't deployed it.

Vanta collects evidence through hundreds of integrations. Here, only the measures are synced from Git, and linking evidence is still manual. Only SOC 2 is mapped so far. The other 12 frameworks are in Probo with all their controls, waiting. Kyverno's policy reports don't reach Wazuh yet either, so I can't show a compliance score per framework the way Azure Policy can, and the admission measure stays "in progress". Falco also isn't running on the database and GPU nodes yet; it covers 14 of the 20 VMs until I add tolerations for those pools.

And you run all of it yourself. Upgrades, storage, certificates and noise tuning are your job. The tuning section above is a fair picture of what that takes.

## Cost and sovereignty

There's no licence fee. What it costs is the capacity it uses, measured on the live cluster:

| Component | Footprint |
| --- | --- |
| Wazuh: 3 indexers, 3 managers, dashboard | about 7 GiB memory, under 0.1 CPU core idle |
| Wazuh storage on Ceph | 3 × 30Gi indexer, 3 × 40Gi manager |
| Falco on 14 nodes | about 28 millicores per node after tuning |
| Probo with its Postgres | about 0.5 GiB memory |

Here's roughly what the same coverage would cost from the vendors, at their published or reported prices:

| Product | Price basis | For this lab |
| --- | --- | --- |
| CrowdStrike Falcon Enterprise (the tier with full EDR) | $184.99 per device per year, list price ([CrowdStrike pricing](https://www.crowdstrike.com/en-us/pricing/)) | 23 hosts: about $4,250 a year. Container and Kubernetes protection is priced separately and isn't published |
| Vanta | No published price. Median contract $20,000 a year; $12,000 to $28,000 for 1 to 50 employees on one framework ([Vendr, from anonymised purchases](https://www.vendr.com/marketplace/vanta)) | at least $12,000 a year, plus the auditor: $8,000 to $25,000+ for SOC 2, not included |
| Azure Arc + Azure Policy | Kubernetes configuration: first 6 vCPUs free per subscription; Azure Policy guest configuration $6 per server per month outside Azure; other Azure Policy at no extra cost ([Azure Arc pricing](https://azure.microsoft.com/en-us/pricing/details/azure-arc/core-control-plane/)) | the smallest line, but every policy and audit log then lives in Azure |

That comes to $16,000 or more a year for a homelab, before anyone has paid an auditor. Self-hosting doesn't get rid of the auditor either.

It doesn't get rid of the penetration test, either. PCI DSS still wants one every year, plenty of customers ask for the report, and third-party testers aren't cheap. In my experience, even moving a booked pentest to another date can cost 25% of the fee. What this stack changes is what you're paying the tester for. The findings a scanner would hand you, like the ICMP-redirect check failing on every node, are fixed before anyone arrives, so the hours go into the problems only a person can find. And while they work, every technique they try should show up in Wazuh and in Slack, which turns the pentest into a test of your detection as well.

For me, though, the stronger reason is sovereignty. My security telemetry and compliance evidence never leave my hardware. No third party processes them, so the question of cross-border transfers never comes up.

That carries more weight in the EU. DORA makes financial firms manage the risk of every ICT provider they rely on and keep a register of them. NIS 2 asks essential and important entities to secure their supply chain and to report a significant incident with an early warning within 24 hours and a fuller notification within 72. A self-hosted stack means fewer providers on that register, and the logs you'd report from stay on your own hardware. Probo ships both frameworks, too.

## What's Next?

The next piece closes the gap in that last step: a small job that pushes Wazuh alert counts for each SOC 2 criterion into Probo as evidence, so the audit view updates itself. After that I'll send Kyverno's policy reports into Wazuh, give Falco tolerations for the database and GPU nodes, and link ISO 27001 to the measures that already exist.

This is the part of the platform that proves the rest of it to someone looking in from outside: the Terraform, RKE2 with Ansible, the hardening series, the signed supply chain, storage, Vault and observability. It does live detection, mapped evidence and audit tracking, all in Git, all on hardware I control.

To follow the rest of this build, 🚀 [**Follow me on LinkedIn**](https://www.linkedin.com/in/george-ezejiofor-89615a8a/)

*Author: George Ezejiofor*
