Building a Kubernetes Security Reporter for Obsidian
Table of Contents
What if your Kubernetes cluster could automatically write security reports directly into your second brain? Today I built exactly thatβa system that runs CIS benchmark scans against my K3s cluster and publishes the results as beautifully formatted Obsidian notes. No more checking dashboards or parsing JSON files; just open your vault and see your cluster’s security posture.
Part 1: The Problem
Security Visibility in a Homelab
Running a Kubernetes homelab means wearing many hats: developer, operator, and security engineer. Tools like kube-bench exist to scan your cluster against CIS benchmarks, but they typically output JSON or text that sits in a PersistentVolume gathering dust.
I wanted:
- Daily automated security scans using K3s-specific CIS benchmarks
- Human-readable reports I could review with my morning coffee
- Integration with my knowledge management system (Obsidian)
- Historical tracking to see security improvements over time
The Architecture
The solution involves three components:
- kube-bench scanner - CronJob running at 2:00 AM, saving JSON results to NFS storage
- kube-bench-reporter - Python app running at 2:30 AM, parsing results and publishing to Obsidian
- Obsidian Local REST API - Plugin exposing your vault via HTTP
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β kube-bench βββββΆβ NFS Storage βββββΆβ Reporter β
β (Scanner) β β (JSON files) β β (Python) β
βββββββββββββββ ββββββββββββββββ ββββββββ¬βββββββ
β
βΌ
βββββββββββββββ
β Obsidian β
β REST API β
βββββββββββββββ
Part 2: Building the Reporter
The Python Script
The reporter is surprisingly simpleβabout 200 lines of Python. Here’s the core logic:
#!/usr/bin/env python3
"""Kube-bench report publisher to Obsidian."""
import os
import json
import requests
import urllib3
from pathlib import Path
from datetime import datetime
# Disable SSL warnings for self-signed certificates
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
RESULTS_DIR = os.getenv("RESULTS_DIR", "/results")
OBSIDIAN_API_URL = os.getenv("OBSIDIAN_API_URL")
OBSIDIAN_API_KEY = os.getenv("OBSIDIAN_API_KEY")
OBSIDIAN_FOLDER = os.getenv("OBSIDIAN_FOLDER", "Cluster/Security")
The script:
- Finds the latest kube-bench JSON results file
- Parses and counts checks by status (PASS, FAIL, WARN, INFO)
- Generates two markdown files: a summary and detailed report
- Publishes both to Obsidian via the REST API
Generating Useful Reports
The summary report includes a table of results and lists failed checks:
def generate_summary(data, date_str):
counts = count_by_status(data)
failed = get_checks_by_status(data, "FAIL")
md = f"""---
date: {date_str}
type: security-summary
tags:
- security
- kube-bench
- cis-benchmark
---
# Kube-Bench Security Summary - {date_str}
## Results Overview
| Status | Count | Percentage |
|--------|-------|------------|
| PASS | {counts['PASS']} | {counts['PASS']/total*100:.1f}% |
| FAIL | {counts['FAIL']} | {counts['FAIL']/total*100:.1f}% |
...
"""
The detailed report includes every check with remediation steps for failuresβperfect for actually fixing issues.
Publishing to Obsidian
The Obsidian Local REST API accepts PUT requests to create or update files:
def publish_to_obsidian(note_path, content):
url = f"{OBSIDIAN_API_URL}/vault/{note_path}"
headers = {
"Authorization": f"Bearer {OBSIDIAN_API_KEY}",
"Content-Type": "text/markdown"
}
response = requests.put(
url,
headers=headers,
data=content.encode('utf-8'),
timeout=30,
verify=False # Self-signed cert
)
response.raise_for_status()
Part 3: Kubernetes Deployment
GitOps with FluxCD
Following GitOps principles, everything is declarative YAML. The reporter CronJob:
apiVersion: batch/v1
kind: CronJob
metadata:
name: kube-bench-reporter
namespace: kube-bench
spec:
schedule: "30 2 * * *" # 30 min after scanner
jobTemplate:
spec:
template:
spec:
containers:
- name: reporter
image: lzetam/kube-bench-reporter:latest
envFrom:
- configMapRef:
name: kube-bench-reporter-config
env:
- name: OBSIDIAN_API_URL
valueFrom:
secretKeyRef:
name: obsidian-api
key: api_url
- name: OBSIDIAN_API_KEY
valueFrom:
secretKeyRef:
name: obsidian-api
key: api_key
volumeMounts:
- name: results-storage
mountPath: /results
readOnly: true
Secret Management with External Secrets
Credentials are stored in AWS Secrets Manager and synced via External Secrets Operator:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: obsidian-api-secret
spec:
secretStoreRef:
name: aws-secret-store
kind: SecretStore
target:
name: obsidian-api
data:
- secretKey: api_url
remoteRef:
key: obsidian/api
property: api_url
- secretKey: api_key
remoteRef:
key: obsidian/api
property: api_key
Part 4: Lessons Learned
Debugging Container Permissions
The first major hurdle was file permissions. The reporter container runs as user 1000 (non-root), but couldn’t read files on the NFS volume. The fix was ensuring the Dockerfile creates the user before copying files:
FROM python:3.12-slim
WORKDIR /app
RUN useradd -m -u 1000 appuser
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=appuser:appuser reporter.py .
USER appuser
ENTRYPOINT ["python", "reporter.py"]
Network Connectivity
Obsidian runs on my local Mac, not in Kubernetes. For pods to reach it:
- The Obsidian REST API must bind to
0.0.0.0(not just localhost) - The API URL in Kubernetes secrets must use the Mac’s network IP
Self-Signed Certificates
The Obsidian REST API uses HTTPS with a self-signed certificate. The reporter needs verify=False in requests and should suppress the resulting warnings:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
Part 5: The Result
Every morning, my Obsidian vault now has fresh security reports in Cluster/Security/:
- Kube-Bench-Summary-YYYY-MM-DD.md - Quick overview with pass rates and failed checks
- Kube-Bench-Details-YYYY-MM-DD.md - Complete report with remediation steps
The summary shows at a glance how my cluster is doing:
## Results Overview
| Status | Count | Percentage |
|--------|-------|------------|
| PASS | 45 | 52.3% |
| FAIL | 18 | 20.9% |
| WARN | 12 | 14.0% |
| INFO | 11 | 12.8% |
And I can click through to the detailed report to see exactly what needs fixing.
Key Takeaways
Automate the boring stuff - Security scanning is only useful if you actually look at the results. Publishing to your daily knowledge system makes it visible.
GitOps everything - Even “utility” workloads benefit from declarative configuration and version control.
Bridge the gap - Tools like kube-bench produce machine-readable output. A simple Python script can transform that into human-readable notes.
Non-root containers need care - File permissions are a common gotcha when running as non-root. Test with debug pods using the same user ID.
Network boundaries matter - When your data consumer (Obsidian) is outside the cluster, you need to think about network accessibility and binding addresses.