Building the Sovereign Foundation: Why We Had to Build the Infrastructure Roads Before the Houses
It's a Saturday night but the new season doesn't start until October! Well, then, time to finally get this set of blog entries out from draft to live, instead.
The ThreatLabs CTI stack started simple enough: a standard open-source sovereign threat intelligence factory running MISP, OpenCTI, TheHive, and DFIR-IRIS. It was stable, purring along beautifully over a unified layout until we tried to hook it into the cloud.
The plan was to let an n8n workflow feed unstructured dark web posts and red team scripts to a cloud LLM provider to extract indicators of compromise (IOCs) and draft intelligence briefs automatically. It worked for about a month, right up until the automated pipeline hit a report detailing a cartel's operational security practices, and the corporate API model threw a safety tantrum and refused to process it. Then it happened again on a standard threat actor TTP analysis.
Corporate cloud LLMs are trained to act like overly sensitive customer-service reps. We needed a cynical forensic analyst who doesn't flinch at raw malicious text. The conclusion was obvious but highly annoying: we had to yank the cords, go completely local, and run our own intelligence fabric on bare metal where no hyperscaler could dictate what our threat data is allowed to look like.
But before we could even load a model, we had to build the roads. And that’s where the infrastructure design fallacies kicked down the door.
Pitfall 1: The "Simple" Network Fallacy (cti-net)
The expectation was classic homelab laziness: spin up a few separate Docker Compose stacks for each application, let them map their default bridges, and expect them to magically resolve each other by hostname.
The messy reality? Independent Compose stacks love creating isolated, siloed networks by default. TheHive couldn’t resolve MISP, the web crawlers were blind, data handoffs were dropping webhooks like crazy, and the whole stack was yelling network timeout errors back into the logs.
We debated the classic architecture fork: a monolithic docker-compose.yml that wraps everything under one giant config versus totally isolated stacks. Monoliths are an absolute nightmare to maintain or update independently; pure isolation breaks the integration entirely.
The fix required a fundamental mental shift—infrastructure has to come first. We had to manually define a shared external system network called cti-net across the host hypervisor space before deploying a single tool. You build the roads before you construct the houses.
The Blueprint
# Manual network provision on the host command line:
# docker network create cti-net
networks:
cti-net:
external: trueVoila! Every service now registers onto the same sovereign highway, resolving each other seamlessly by their internal container handles. Network fallacy solved. Volume permissions? That was a far uglier beast.
Pitfall 2: The Volume Permission Nightmare
Docker makes downloading enterprise software incredibly lazy, right up until you try to persist the data onto physical host disks and Linux file permissions turn into absolute hell.
When you scale a CTI stack, you're dealing with disparate upstream container standards. PostgreSQL runs natively as UID 70. ElasticSearch wants to run as UID 1000. Redis drops onto another custom UID entirely. The second you map these container directories to persistent host folders on your local NVMe storage array, the ownership structures mismatch, the deployment drops into a CrashLoopBackOff, and the console logs start screaming Permission denied.
I've seen folk try to bypass this by blindly running chmod 777 across their entire storage pool. Don't do that. It’s a lazy, shameful quick-fix that completely breaks file system security and leaves your underlying infrastructure naked.
We needed automation, not manual dirty hacks. The turning point was crafting a dedicated baseline janitor script—fix-permissions.sh—that executes at the host level right before the stack is brought up. It parses our targeted database and directory layouts, forcefully aligning the host folder ownership parameters to match exactly what the internal container engines require.
The Workaround: fix-permissions.sh
#!/bin/bash
# fix-permissions.sh: Host-level janitor script for the CTI storage layout
echo "Automating volume ownership parameters for cti-net stack..."
# Hard-set exact paths relative to your local storage mount
CTI_DATA_DIR="/opt/stacks/cti-data"
# PostgreSQL volume alignment (UID 70)
sudo chown -R 70:70 ${CTI_DATA_DIR}/postgres
# ElasticSearch volume alignment (UID 1000)
sudo chown -R 1000:1000 ${CTI_DATA_DIR}/elasticsearch
# Redis volume alignment
sudo chown -R 999:999 ${CTI_DATA_DIR}/redis
echo "Volume ownership verified. Keys to the castle distributed safely."Now, we wrap this step directly into our host deployment workflow. No manual directory tracking, no security compromises, and no more silent volume launch failures.
With the roads laid and the volume maps behaving themselves, Act I is officially on the grid. Next up in Post 2, we’re tackling what happens when MISP and Traefik get into an existential fight over port mapping, Nginx falls into an infinite redirection loop, and ElasticSearch tries to eat every byte of system memory on the server array.
Until then, see ya later. Happy tinkering!