ADR-0083: KALI.Art Phase B — production hardening + MCP + persistence¶
- Status: Accepted
- Date: 2026-05-23
- Driver: ADR-0082 closed Phase A "Negative / deferred" with a 5-item list (PIE workflow, ttyd iframe, real MCP bridge, audit log + PDF wiring, PVC
kali-home). Phase B finishes that list and adds the operator-hygiene CronJobs (image refresh + quota scan) that were always in the 12-PR memo but landed late. - Related: PRs #1016 (K-B1), #1017 (K-B2), #1018 (K-B3), #1019 (K-B4), #1020 (K-B5), #1021 (K-B6), #1022 (K-B7), this PR (K-B8); ADR-0082 (Phase A locked decisions), ADR-0029 (audit hash-chain pattern), ADR-0017 (backup/DR — 90 d retention), ADR-0045 (OOBI Hello mesh — origin of the per-namespace mTLS pattern used by the K-B4 ingest endpoint).
Context¶
Phase A made KALI.Art operator-usable end-to-end but parked five known gaps:
- PIE workflow —
dom_mode='production'returned403 prod-dom-blockedunconditionally - ttyd iframe — session page surfaced "Session id" + a
terminal pendingplaceholder - MCP bridge —
/api/kali/ai-companionwas a 4-bucket regex stub; no real Anthropic Model Context Protocol surface - Audit log + PDF — schemas existed in
dashboard/src/lib/kali/; transport + renderer didn't - PVC
kali-home—/home/operatorwas a 1 GiBemptyDir(no persistence across pod restarts)
Phase B also picked up two operator-hygiene jobs that the 12-PR memo flagged (image refresh + quota scan) — they are the CronJob complement to the K-B6 persistence layer.
Decision¶
Ship Phase B as 8 sub-PRs following the K-Ax cadence. Each sub-PR is independently reviewable; the order matters only for K-B4 (depends on K-B1/K-B3 for the new audit event kinds) and K-B7 (depends on K-B6 for the homes store).
Phase B sub-PRs¶
| Sub-PR | Scope | Status |
|---|---|---|
| K-B1 | PIE workflow: Drizzle migration 0033_kali_pie_approvals, pie-store.ts (in-mem dual-mode-ready), 3 API routes (request/approve/deny), session-create guard. Two-person rule (CHECK approver <> operator). 60 min unlock window. Read-only-tools allow/block lists. |
✅ #1016 |
| K-B2 | ttyd iframe via URL resolver. New API /api/kali/sessions/[id]/ttyd returns {url, embeddable, reason?} from KALI_TTYD_PUBLIC_URL / KALI_TTYD_BASE_URL env vars. TtydEmbed.tsx component with TanStack Query 5s polling + restrictive iframe sandbox. WebSocket reverse-proxy delegated to nginx/traefik (Next.js can't proxy WS cleanly). |
✅ #1017 |
| K-B3 | Real MCP bridge sidecar — new Go module kali-art-mcp/ with HTTP :8085 exposing Anthropic MCP 2024-11-05 subset (initialize, tools/list, tools/call, resources/list). 4 tools (3 proxied to kali-art /rpc + 1 synthetic suggest-workflow). Dashboard AI Companion forwards through it when KALI_MCP_BRIDGE_URL is set; falls through to the local stub on any failure. |
✅ #1018 |
| K-B4 | Audit log transport + dashboard UI. Drizzle migration 0034_kali_audit_events, audit-store.ts with SHA-256 hash chain mirroring netsecopen/audit-chain.ts. 6 dashboard event kinds emitted from existing routes. Admin page /art-studio/kali/audit with TanStack 5 s polling + chain verifier. |
✅ #1019 |
| K-B5 | Session PDF report wiring. pdf-report-renderer.ts reuses the DoYour B-2 pdfkit pipeline; 4-section layout (Header / Summary / Findings / Audit chain). New endpoint GET /api/kali/sessions/[id]/report.pdf with Content-Disposition: attachment. "Download PDF" anchor on the session list. |
✅ #1020 |
| K-B6 | PVC kali-operators-home (50 GiB RWO) replaces the emptyDir. Drizzle migration 0035_kali_operator_homes, operator-home-store.ts with tier quotas (Team 1 GiB / Enterprise 5 GiB), ensureHome() on session start, 4 admin API routes. Launcher mkdir per-operator subdir. |
✅ #1021 |
| K-B7 | Operator-hygiene CronJobs. 93-kali-image-refresh.yaml becomes a real buildah pull/overlay/push/rollout (was placeholder). New 94-kali-quota-scan.yaml daily du -sb /home/* → POST to dashboard. Endpoint POST /api/kali/homes/scan. |
✅ #1022 |
| K-B8 | This PR — ADR + module-docs update in 3 langs + recovery-memo entry. | ✅ |
Locked defaults — Q1 quota revised¶
ADR-0082 Q1 quotas locked as Team 1 GiB / Enterprise 10 GiB. K-B6 ships Team 1 GiB / Enterprise 5 GiB to stay within the 50 GiB PVC budget for the first deployment wave (~6 Enterprise homes fit comfortably; at 10 GiB it would be 4). The 10 GiB cap is achievable by expanding the PVC to 100 GiB in an operator-controlled overlay; no code change needed.
MCP bridge protocol choice¶
K-B3 implements the Anthropic Model Context Protocol subset (version 2024-11-05). The discuss memo's "Addendum 1" called for a fork of mcp-kali-server upstream; instead, we ship a clean Go re-implementation matching the same MCP wire shape so Claude Desktop, 5ire, and any MCP client point at the bridge without modification. The bridge proxies the 3 real Kali tools to kali-art /rpc (default-deny ACL enforced both sides) and handles suggest-workflow synthetically (server-side 4-bucket regex matching the K-A3 stub).
Hash-chain pattern¶
K-B4 reuses netsecopen/audit-chain.ts primitives (canonicalize, sha256Hex, GENESIS_PREV_HASH) so every offensive-suite event stream shares the same SHA-256 chain semantics. entry_hash = SHA-256(prev_hash || canonicalize(payload) || timestamp || sequence). The K-B4 admin UI's "Verify chain" button re-walks the sequence and surfaces tampering with the failing sequence numbers.
Consequences¶
Positive
- Every Phase A "deferred" item is now closed. KALI.Art is feature-complete for the v3.13.0 milestone.
- The audit chain pattern is now shared between NetSecOPEN reports + KALI sessions + (next) DoYour ArtPiece runs — single tamper-evidence convention across the offensive-suite.
- The MCP bridge unlocks the "point Claude Desktop at our cluster" demo without any dashboard changes, broadening the operator-tooling ecosystem.
- Per-operator persistence (K-B6) finally makes the KALI shell feel like a real machine — apt installs + sudoers tweaks + bash history all survive pod restarts.
- The CronJobs (K-B7) make the loop self-sustaining: fresh images weekly, quota usage tracked nightly, no manual cluster babysitting.
Negative / deferred (Phase C / D)
- noVNC for GUI tools (Burp / Wireshark / Zenmap) — Phase C (v5.6). Requires a sibling websocket service and an Xvfb-backed display inside the pod.
- Cross-MÓDULO orchestration extras beyond DoYour↔KALI (e.g. KALI nmap output flowing into PURE Discovery Hub as source #8) — Phase D (v6.0+). The K-10 sub-PR from the original 12-PR roadmap is the carrier.
- Per-keystroke audit recording (Enterprise tier add-on) — the K-B4 chain is summary-only by default. The shell-history hash is a sibling table addition (no migration to the existing chain).
- K-A2
/rpctool catalogue expansion beyond nmap / sslyze / nikto — driven by customer requests. Each new tool is one Go handler + one ACL entry + one MCPtools/listentry. - PIE approval audit trail — K-B1 emits
pie-requested/pie-approved/pie-deniedevents into the K-B4 chain (wired in K-B4 itself), but a dedicated PDF "PIE history per DUT" report is deferred to K-B8 follow-up.
Alternatives considered¶
- Single mega-PR for Phase B (~3500 LoC) — rejected. The 8-sub-PR cadence keeps each merge small + reviewable + revertable, matching the Phase A cadence + the project-wide stacked-PR pattern.
- Fork
mcp-kali-serverupstream (per discuss memo Addendum 1) — rejected. Clean Go re-impl is ~400 LoC + 6 tests, no licence-attribution surface, and lets us adapt the protocol version + tool catalogue independent of upstream's roadmap. - One PVC per operator — rejected. Operators come and go; managing 100+ PVCs adds cluster churn without buying isolation we need (the kali-art pod is already a single-tenant gVisor sandbox with per-session ttyd processes). subPath separation inside a single 50 GiB PVC is sufficient.
- Build the PIE workflow as a separate microservice — rejected. PIE is a 2-table state machine that lives naturally in the same Postgres as the audit chain + sessions. A separate service would add ops cost without buying isolation we need.
- Generate the PDF report client-side — rejected. Server-side pdfkit (already shipping for DoYour B-2) keeps the bytes deterministic across browsers + lets us hash the rendered output into the audit chain (future K-B5 follow-up).
Compliance¶
Per CLAUDE.md: "Dashboard is the ONLY operator interface." Every Phase B feature ships with a dashboard surface (PIE: admin queue page; ttyd: iframe; MCP: same AI Companion sidebar; audit: new admin page; PDF: download anchor; homes: 4 API endpoints + admin list).
Per ADR-0044 (OOBI Intrusion Detection): the K-B4 ingest endpoint (POST /api/kali/audit/ingest) is bound to the cluster-internal dashboard.web-agents.svc.cluster.local:3000 only; the K-B7 quota-scan CronJob's kali-quota-scan ServiceAccount is namespace-scoped to web-agents.
Per the locked ZTP-prem posture: gosec W0 baseline stays at 0 findings post-merge. The 2 inline #nosec annotations added across Phase B (G107 on the MCP bridge proxy http.Post, G301 on the launcher's /home/<operator> mkdir) are documented per site.
References¶
- discuss_module_kali_art_2026_05_10
- ADR-0025 — offensive-suite parent
- ADR-0082 — Phase A locked decisions
- ADR-0029 — audit hash-chain pattern
- ADR-0017 — backup/DR (90 d retention)
- PRs #1016–#1022, plus this PR — K-B1 through K-B8