Skip to main content

Command Palette

Search for a command to run...

Deploying OSS Kubernetes Security Console

Welcome to the Jungle

Updated
10 min read
Deploying OSS Kubernetes Security Console
M

Working as a solutions architect while going deep on Kubernetes security — prevention-first thinking, open source tooling, and a daily rabbit hole of hands-on learning. I make the mistakes, then figure out how to fix them (eventually).

The first post laid out the idea: Kubernetes security tools already produce a lot of useful data, but most of it lands in separate places.

Trivy Operator writes vulnerability and configuration findings into CRDs. Kubescape writes posture and compliance results. Kyverno writes policy reports. Falco emits runtime events. Each tool is useful on its own, but the real value starts when you can pull those signals into one place and ask better questions across them.

This post is the deploy. You can clone the repo and run the stack yourself. Or, if you do not want to spin up a cluster right now, you can still follow the story and see what each piece contributes.

The main point is simple. helm install is not the finish line. The finish line is security data flowing.

What gets deployed

The repo uses a Helm umbrella chart to deploy the core stack:

  • Trivy Operator
  • Kubescape Operator
  • Falco
  • Falcosidekick
  • Kyverno
  • A small MCP event sink for Falco events

The goal is not to build a perfect enterprise platform in one command. That would be suspicious. The goal is to get a working local security data plane.

After the install, the cluster has tools that can answer questions like:

  • What vulnerable images are running?
  • Which workloads have risky configuration?
  • Which Kyverno policies are failing?
  • Are runtime events being generated?
  • Can an MCP server summarize this without dumping raw YAML into the model?

That last part matters. The Kubernetes API can return a mountain of YAML. The model does not need all of it. The model needs useful summaries.

Deployed is not the same as running

This is the first thing to understand with a stack like this. A Helm chart can install successfully while the security data is still empty. That does not always mean something is broken.

Most Kubernetes security operators follow the same rough pattern:

  1. Install the operator.
  2. Register CRDs.
  3. Watch workloads.
  4. Launch scans or controllers.
  5. Write results back into Kubernetes.

There is a delay between "the pod is running" and "the finding exists." So after deployment, I do not start by asking kube basic questions:

kubectl get pods -A

That is useful, but it only tells me the components are alive.

The better question is:

kubectl get vulnerabilityreports -A
kubectl get policyreports -A
kubectl get clustercompliancereports

That tells me whether the stack is producing data. Pods are table stakes. Findings are the point.

The deploy flow

The short version looks like this:

kubectl cluster-info

git clone https://github.com/sf-matt/k8s-sec-stack
cd k8s-sec-stack

./hack/bootstrap.sh
kubectl apply -f demo/
./hack/configure-local.sh

Then restart Claude Code from the project directory so it picks up the generated MCP config. That is the happy path. Under the hood, bootstrap.sh is doing the normal Helm work:

helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo add aqua https://aquasecurity.github.io/helm-charts
helm repo add kubescape https://kubescape.github.io/helm-charts
helm repo add kyverno https://kyverno.github.io/kyverno
helm repo update

helm dependency update ./charts/k8s-sec-stack

helm upgrade --install k8s-sec-stack ./charts/k8s-sec-stack \
  --namespace security \
  --create-namespace \
  --wait \
  --timeout 10m

The umbrella chart keeps the install simple. One chart pulls in the tool-specific charts and passes configuration down to each subchart. That gives us one repeatable entry point instead of four separate install guides taped together.

The demo workload

After the stack is deployed, the repo applies demo workloads:

matt.brown@matt k8s-sec-stack % kubectl apply -f demo/
namespace/demo configured

These workloads are intentionally not clean. That is the point. A perfectly clean demo cluster is very calming, and also useless. The demo workloads give the tools something to detect:

  • Trivy can find image vulnerabilities.
  • Kubescape can flag insecure workload configuration.
  • Kyverno can produce policy violations.
  • Falco can generate runtime events when we interact with the workload.

This turns the stack from "installed tools" into "tools producing evidence."

Verifying Trivy Operator

Trivy Operator writes results into CRDs. The first check is whether the operator is running:

matt.brown@matt k8s-sec-stack % kubectl get deploy k8s-sec-stack-trivy-operator -n security
NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
k8s-sec-stack-trivy-operator   1/1     1            1           9d

Then check whether reports are appearing:

matt.brown@matt k8s-sec-stack % kubectl get vulnerabilityreports -A | head -5
NAMESPACE            NAME                                                              REPOSITORY                         TAG           SCANNER   AGE
calico-apiserver     replicaset-calico-apiserver-6db954fd57-calico-apiserver           calico/apiserver                   v3.25.0       Trivy     21m
calico-system        daemonset-calico-node-calico-node                                 calico/node                        v3.25.0       Trivy     25m
calico-system        daemonset-csi-node-driver-calico-csi                              calico/csi                         v3.25.0       Trivy     23m
calico-system        daemonset-csi-node-driver-csi-node-driver-registrar               calico/node-driver-registrar       v3.25.0       Trivy     23m
matt.brown@matt k8s-sec-stack % kubectl get configauditreports -A | head -5
NAMESPACE            NAME                                                  SCANNER   AGE
calico-apiserver     networkpolicy-allow-apiserver                         Trivy     9d
calico-apiserver     replicaset-calico-apiserver-6db954fd57                Trivy     9d
calico-apiserver     service-calico-api                                    Trivy     9d
calico-system        daemonset-calico-node                                 Trivy     9d

If those are empty immediately after install, wait a few minutes. Trivy Operator needs time to watch workloads, create scan jobs, run scans, and write results. If they are still empty after several minutes, check the operator logs:

kubectl logs -n security deploy/k8s-sec-stack-trivy-operator

This is the pattern for the whole stack. Do not stop at "the deployment exists." Check that the deployment is producing the thing you actually need.

Verifying Kubescape

Kubescape has more moving parts. It includes scanner components, storage, vulnerability scanning, compliance reporting, and a node agent.

Start with:

kubectl get deploy -n kubescape
kubectl get pods -n kubescape

Then check for compliance reports:

matt.brown@matt k8s-sec-stack % kubectl get clustercompliancereports
NAME                     AGE
k8s-cis-1.23             9d
k8s-nsa-1.0              9d
k8s-pss-baseline-0.1     9d
k8s-pss-restricted-0.1   9d

The useful check is the report status:

matt.brown@matt k8s-sec-stack % kubectl get clustercompliancereport k8s-nsa-1.0 -o json | jq '.status.summary'
{
  "failCount": 12,
  "passCount": 15
}

You want to see pass and fail counts. If the report exists but the counts are zero, it may not be done yet.

matt.brown@matt k8s-sec-stack % kubectl get clustercompliancereport k8s-nsa-1.0 -o json | jq '.status'
{
  "summary": {
    "failCount": 12,
    "passCount": 15
  },
  "summaryReport": {
    "controlCheck": [
      {
        "id": "1.0",
        "name": "Non-root containers",
        "severity": "MEDIUM",
        "totalFail": 27
      },
...

Verifying Kyverno

Kyverno gives us admission control and policy reporting.

First check the controllers:

kubectl get deploy -n security | grep kyverno

Then check that the webhook exists:

matt.brown@matt k8s-sec-stack % kubectl get validatingwebhookconfigurations | grep kyverno
kyverno-cel-exception-validating-webhook-cfg    1          9d
kyverno-cleanup-validating-webhook-cfg          1          9d
...

Then check for policy reports:

matt.brown@matt k8s-sec-stack % kubectl get policyreports -A | head -5
NAMESPACE            NAME                                   KIND         NAME                                             PASS   FAIL   WARN   ERROR   SKIP   AGE
calico-apiserver     2d6b9b43-432f-4475-82cc-564f3efcdbbe   Pod          calico-apiserver-6db954fd57-xz6l5                3      4      0      0       0      9d
calico-apiserver     7e6a00c6-fa0e-419e-ab10-ffccd068983a   Pod          calico-apiserver-6db954fd57-v4klw                3      4      0      0       0      9d
calico-apiserver     e1f55f30-87dc-433e-9820-2210fad5d5c8   Deployment   calico-apiserver                                 3      4      0      0       0      9d
calico-apiserver     f8f9db44-0fd9-4bef-80eb-8c9418b5f6cf   ReplicaSet   calico-apiserver-6db954fd57                      3      4      0      0       0      9d

And count failed policy results:

matt.brown@matt k8s-sec-stack % kubectl get policyreports -A -o json | \
  jq '[.items[].results[] | select(.result=="fail")] | length'
207

This is where the console idea starts to become more obvious. Kyverno and Kubescape can both produce policy-style findings. Instead of treating them as totally separate worlds, the MCP layer can summarize those findings into a shared view. That is the beginning of an actual security console. A place where signals can be queried, summarized, and used.

Verifying Falco

Falco is different from the others. Trivy, Kubescape, and Kyverno write structured results into Kubernetes CRDs. Falco emits runtime events. So the verification path is different. Check that Falco is running on each node:

kubectl get ds k8s-sec-stack-falco -n security

In this stack, Falco sends events to Falcosidekick, and Falcosidekick forwards them to a small event sink. The sink stores events so the MCP server can query them later.

Trigger a basic event:

kubectl run falco-test-shell --image=alpine --restart=Never --rm -- sh -c 'whoami' && sleep 5 && curl -s http://\(NODE_IP:\)NODE_PORT/events | jq -r '.[].rule' | sort | uniq -c | sort -nr | head

Then check the sink:

matt.brown@matt k8s-sec-stack % NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
NODE_PORT=$(kubectl get svc mcp-event-sink -n security -o jsonpath='{.spec.ports[0].nodePort}')
matt.brown@matt k8s-sec-stack % curl -s http://\(NODE_IP:\)NODE_PORT/events | jq '.[-1] | {
  time: .time,
  rule: .rule,
  priority: .priority,
  output: .output
}'
{
  "time": "2026-06-03T06:09:21.286989548Z",
  "rule": "Terminal shell in container",
  "priority": "Notice",
  "output": "06:09:21.286989548: Notice A shell was spawned in a container with an attached terminal | evt_type=execve user=root user_uid=0 user_loginuid=-1 process=sh proc_exepath=/bin/busybox parent=containerd-shim command=sh -c whoami terminal=34816 exe_flags=EXE_WRITABLE|EXE_LOWER_LAYER container_id=5df14fcd55f9 container_name=test-shell container_image_repository=docker.io/library/alpine container_image_tag=latest k8s_pod_name=test-shell k8s_ns_name=default"
}

That is the Falco equivalent of “the CRD has data.”

Connecting the MCP server

The MCP server runs locally. It is not deployed inside the cluster. That is intentional. It connects to Kubernetes through your local kubeconfig, queries the cluster API, and exposes a smaller set of tools to Claude Code.

Run:

./hack/configure-local.sh

That writes local MCP config for the project:

.mcp.json
.claude/settings.json

These files are machine-specific and gitignored. The generated config tells Claude Code how to launch the MCP server and where to find the Falco event sink. After that, restart Claude Code from the repo directory.

Then check:

/mcp

If the tools show up, Claude Code can talk to the MCP server. If they do not, run the server manually:

uv run --project mcp-server k8s-sec-mcp

Common issues are usually boring:

  • wrong working directory
  • kubeconfig pointed at the wrong cluster
  • missing Python dependency
  • MCP SDK version mismatch

Boring issues still count.

Why summarize instead of dumping YAML? (This just changed!)

The MCP tools are not just wrappers around kubectl get. That would technically work, but it would be expensive and noisy. A raw Trivy VulnerabilityReport can be hundreds of lines. A cluster can have many of them. Most of that data does not need to go into the model.

The MCP tool should return the useful part:

{
  "image": "sfmatt/flask-vuln-demo:latest",
  "critical_count": 2,
  "high_count": 8,
  "fixable_count": 3,
  "unfixable_count": 7
}

That is the design principle for this project:

  • Deterministic aggregation belongs in the tool.
  • Reasoning belongs in the model.

So the MCP server should handle things like:

  • counting findings
  • grouping by image
  • deduplicating repeated CVEs
  • summarizing policy failures
  • filtering noisy raw fields

The model should handle things like:

  • explaining what matters
  • suggesting priority
  • connecting related signals
  • generating next-step remediation ideas

That boundary is the difference between an MCP server that helps and an MCP server that blasts out input tokens.

The practical checklist

After deployment, we proved four things:

  • Trivy data exists
  • Kubescape data exists
  • Kyverno data exists
  • Falco events exist

If those are working, the stack is no longer just installed. It is producing security signals. That is the minimum viable console layer.

Wrap Up

At this point, we have:

  • deployed the stack
  • added demo workloads
  • verified that each tool produces data
  • connected a local MCP server
  • started translating raw security objects into summaries

This is still early. It is not a polished product. It is not trying to be one yet.

But it is enough to prove Kubernetes security tools can produce structured signals and MCP tools can summarize those signals. Then the LLM can reason across them without needing every raw CRD dumped into context.

The next step is making the MCP layer more intentional. We will add better tools, better summaries, and better workflows for turning scattered findings into actual security decisions.