Post 022
The Problem: Bot Sprawl
The Alliance Fleet Discord server was accumulating single-purpose bots. Admiral Ackbar for alerts, BD-1 for knowledge queries, separate webhook endpoints for each automation. Each bot had its own runtime, its own credentials, and its own attack surface.
K-2SO is the consolidation play: a unified observability bot that replaces the sprawl with a single, heavily isolated entry point. It reads everything, queries anything, and can't modify a thing.
Design Principles
-
Read-only by default - K-2SO can query every system (Wazuh, InfluxDB, Proxmox API, Docker) but cannot modify any of them. No write permissions, no restart capabilities, no config changes.
-
Double isolation - Docker inside an unprivileged LXC container. Even if the bot is compromised, the attacker is inside a container inside a container with no network path to management interfaces.
-
HTTPS-only API access - K-2SO reaches backend services exclusively via HTTPS API endpoints. No SSH, no direct database connections, no shell access.
-
Dedicated network segment - The bot runs on a Bot-Net VLAN with firewall rules that only allow outbound HTTPS to specific service IPs.
Architecture
┌──────────────────────────────────────────────────┐
│ Proxmox Host (Node-B) │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Unprivileged LXC Container │ │
│ │ (Bot-Net VLAN) │ │
│ │ │ │
│ │ ┌───────────────────────────────────────┐ │ │
│ │ │ Docker Container │ │ │
│ │ │ (K-2SO Bot Runtime) │ │ │
│ │ │ │ │ │
│ │ │ Node.js + Discord.js │ │ │
│ │ │ ├── HTTPS → Wazuh API │ │ │
│ │ │ ├── HTTPS → InfluxDB API │ │ │
│ │ │ ├── HTTPS → Proxmox API │ │ │
│ │ │ ├── HTTPS → Portainer API │ │ │
│ │ │ └── HTTPS → n8n Webhooks │ │ │
│ │ └───────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
Why Double Isolation?
Layer 1 - Unprivileged LXC: The container runs with user namespace mapping. Root inside the LXC is mapped to a non-root UID on the host. Even a container escape doesn't grant host-level access.
Layer 2 - Docker inside LXC: The bot process runs in a Docker container inside the LXC. This adds filesystem isolation, network namespace separation, and resource limits.
Combined effect: An attacker who compromises K-2SO's application code would need to escape Docker, then escape the unprivileged LXC, then pivot across VLANs - each with its own isolation boundary.
Network Security
Bot-Net VLAN
K-2SO's LXC container lives on a dedicated VLAN with restrictive firewall rules:
VLAN: Bot-Net
Subnet: 192.168.50.0/24 (example)
Allowed outbound:
→ 192.168.20.30:443 (Wazuh API)
→ 192.168.20.41:8086 (InfluxDB API)
→ 192.168.20.10:9443 (Portainer API)
→ 192.168.1.x:8006 (Proxmox API)
→ Discord API (external HTTPS)
Denied:
→ All SSH (port 22) to any destination
→ All management interfaces not explicitly listed
→ All inter-VLAN traffic not explicitly allowed
Read-Only API Tokens
Every backend API token used by K-2SO is scoped to read-only:
| Service | Token Scope | Can Read | Can Write |
|---|---|---|---|
| Wazuh | API read | ✅ Alerts, agents, CVEs | ❌ |
| InfluxDB | Read bucket | ✅ Metrics queries | ❌ |
| Proxmox | PVEAuditor role | ✅ Node status, VM info | ❌ |
| Portainer | Read-only user | ✅ Container status | ❌ |
AI Routing via n8n
K-2SO routes AI-powered commands through n8n webhooks rather than calling Ollama directly. This adds an authentication layer and centralizes AI workflow logic:
const AI_COMMANDS = {
'!ask': { command: 'ask', description: 'Ask Ollama a question' },
'!rag': { command: 'rag', description: 'Query documents via AnythingLLM' },
'!imagine': { command: 'imagine', description: 'Generate image via ComfyUI' },
'!models': { command: 'models', description: 'List available Ollama models' },
'!gpu': { command: 'gpu', description: 'Show GPU utilization' },
};
async function handleAICommand(message) {
const response = await fetch(N8N_WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-K2SO-Token': K2SO_WEBHOOK_SECRET,
},
body: JSON.stringify({
command: AI_COMMANDS[cmd].command,
query,
user: message.author.username,
channel: message.channel.name,
timestamp: new Date().toISOString(),
}),
});
// Format and return response to Discord
}
n8n Workflow: AI Router
K-2SO webhook → n8n
├── !ask → Ollama (llama3:8b) → formatted response
├── !rag → AnythingLLM RAG API → context-aware answer
├── !imagine → ComfyUI → generated image
├── !models → Ollama /api/tags → model list
└── !gpu → InfluxDB nvidia_smi query → stats
Conversation Memory (PostgreSQL)
For threaded conversations, K-2SO stores message history in PostgreSQL:
CREATE TABLE k2so_conversations (
id SERIAL PRIMARY KEY,
thread_id VARCHAR(64) NOT NULL,
role VARCHAR(16) NOT NULL,
content TEXT NOT NULL,
model VARCHAR(32),
tokens_used INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_thread_recent
ON k2so_conversations(thread_id, created_at DESC);
-- Auto-cleanup: messages older than 7 days
DELETE FROM k2so_conversations
WHERE created_at < NOW() - INTERVAL '7 days';
Why This Matters for the Portfolio
The K-2SO architecture demonstrates security thinking beyond "it works":
- Least privilege - every API token is scoped to minimum required access
- Defense in depth - two isolation layers plus network segmentation
- Blast radius containment - compromise of the bot limits damage to read-only API access from an isolated VLAN
- Separation of concerns - the bot handles interaction, n8n handles orchestration, backend services handle data
These are the same principles behind Azure Managed Identities, AWS IAM roles, and GCP Workload Identity - applied at the homelab level.
Current Status
K-2SO is in design/planning phase. The architecture document is complete. BD-1 currently handles the interactive Discord role; K-2SO would eventually replace it as the unified observability interface.
Related: Post 021 - BD-1 Discord Bot | Post 020 - n8n Automation Platform | Post 024 - Discord as an Ops Console