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:

  1. Docs and notes are embedded and searchable semantically and via FTS.
  2. Every LLM session has access to the full tool set — search, shell, git, DB — without any manual copy-paste.
  3. Findings feed back into the knowledge base. A resolved vuln becomes a note, gets embedded, and shows up in future sessions on similar targets.
  4. 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

Date: 2026-06-16

Author: Connal McInnis

Created: 2026-06-16 Tue 02:53