bee-stack: a homelab for bug bounty research
Table of Contents
1. The problem
No broadband. Rural NC. One Android phone tethered over USB.
2. The build
A Raspberry Pi 4 runs hostapd and dnsmasq, WiFi AP, DHCP, and NAT for the cluster. A TP-Link managed switch connects three nodes. All internet exits through the phone.
Three nodes run Talos Linux (v1.12.6, Kubernetes v1.35.2):
[ WAN / Internet ]
│ (USB Tethering via Android / Mullvad)
┌──────▼────────────────────────────────────────┐
│ LoqArch Workstation (Arch Linux + Doom Emacs) │
└──────┬────────────────────────────────────────┘
│ (wlp8s0 / 10.10.10.243)
┌──────▼────────────────────────────────────────┐
│ Raspberry Pi 4 (AP + NAT + dnsmasq) │
└──────┬────────────────────────────────────────┘
│ (eth0 / 10.10.10.1)
[ TP-Link Managed Switch ]
│
├─► talos-cp (10.10.10.10) ── k8s Control Plane
│
├─► talos-w1 (10.10.10.11) ── Ollama (mirror), Qdrant, Storage
│
└─► talos-w2 (10.10.10.12) ── Ollama (primary), Gitea, Postgres, bee-tools
Everything uses local-path RWO PVCs. No cloud. No NFS. No CNI drama. Just pure pain and suffering.
3. bee-stack
The “beeattack” namespace is the whole point. All services are internal; NodePorts forward through the control-plane IP.
3.1. Ollama
Two instances sharing model storage via PVCs. Primary on w2, mirror on w1.
Custom model bee-raw — derived from a fine-tuned triage base, system-prompted for direct security research with no disclaimers, temperature 0.2, 8k context. Built inside the pod with a Modelfile.
Also runs gemma4 and nomic-embed-text for embeddings.
3.2. Qdrant
Three collections: docs (reference material, 768-dim cosine), notes (org-denote vault + research notes), code (gitea repo contents). All ingested via a Python chunker that calls the nomic embedding model through Ollama’s API.
Gotchas worth noting: Qdrant 1.17+ changed the upsert format. The old points key breaks silently on some operations. WAL corruption happens if you patch the deployment before the pod finishes terminating — wait for full pod termination before reapplying.
3.3. Postgres
Structured side of the research store: findings, targets, LLM sessions, research notes, doc full-text with a tsvector index, and a tool call log. Postgres FTS complements the Qdrant semantic search — exact-match queries go to Postgres, fuzzy conceptual queries go to Qdrant.
Port-forwarded on 15432 locally. NodePort is technically open but firewalled by Talos.
3.4. Gitea
Self-hosted git. Registration disabled. Push webhook fires on every commit → bee-tools re-ingests changed files into qdrant/code. Token stored at ~/.config/bee-tools-token.
3.5. bee-tools
FastAPI bridge. The connective tissue.
Endpoints cover: RAG search (single collection and merged multi-collection), git file read/write/search, Postgres note and query operations, session save/load, doc ingest and FTS, bug bounty findings CRUD, targets, tool call logging, and a browser UI.
Source lives in ~/Homelab/bee-stack/bee-tools.py. Deployed via configmap — edit file, recreate configmap, rollout restart. One rule: all route definitions must appear before the if __name__ block. FastAPI silently drops anything after it.
4. Emacs integration
The workstation (LoqArch, Arch Linux) runs Doom Emacs. All of the above surfaces through gptel via a custom config at ~/.config/doom/custom/llm-gptel.el.
Keybinds live under SPC l:
| Key | Action |
|---|---|
SPC l l |
Open gptel chat buffer |
SPC l B |
Switch to bug bounty mode |
SPC l T |
Start recon on target domain |
SPC l R |
Toggle RAG auto-inject |
SPC l q |
Manual RAG search → *bee-rag* buffer |
SPC l Q |
Postgres FTS → *bee-docs* buffer |
SPC l w |
Web search → *bee-web* buffer |
SPC l A |
Security analysis of current buffer/region |
SPC l e |
Run shell command on LoqArch |
SPC l F |
Save finding to Postgres |
SPC l L |
List findings → *bee-findings* buffer |
RAG auto-inject runs before every prompt: pulls the top-scoring chunks from Qdrant across all three collections, filters below a 0.72 score threshold, injects up to 5 chunks into the system context. The model sees relevant docs without being asked.
gptel tools (callable by the model during a session): rag_search, doc_search, web_search, url_fetch, shell_exec, file_read, file_write, git_read, git_write, git_search, db_note, db_query, vuln_finding.
5. Bug bounty workflow
The whole stack exists to support this:
SPC l B → bug bounty mode (no-disclaimer system prompt) SPC l T → enter target → LLM recon analysis auto-starts SPC l w → search target intel SPC l W → fetch JS, pages, endpoints SPC l e → run local tools (curl, ffuf, nmap) SPC l A → analyze interesting code or responses SPC l F → confirmed finding → save to Postgres SPC l L → review all findings SPC X v → org-capture detailed report
Findings track: target, title, severity, description, PoC, impact, CVSS, status, and bounty paid. Status flow: new → triaged → accepted → resolved (or duplicate / wontfix).
6. What actually makes this interesting
Most homelab LLM setups front-end a single Ollama instance. This one is built around a research loop:
- Docs and notes are embedded and searchable semantically and via FTS.
- Every LLM session has access to the full tool set — search, shell, git, DB — without any manual copy-paste.
- Findings feed back into the knowledge base. A resolved vuln becomes a note, gets embedded, and shows up in future sessions on similar targets.
- The whole thing runs on a tethered phone connection. Nothing about the stack assumes reliable or fast internet.
The constraint shaped the design. Local-first wasn’t a preference — it was the only option.
7. Code
bee-stack (self-hosted, not public)
~/Homelab/bee-stack/
├── bee-tools.py # FastAPI service
├── bee-stack.sh # deploy postgres + bee-tools
├── bee-tools-bb.sh # deploy findings/targets schema + elisp
└── fix-bee-tools.sh # clean rebuild + redeploy