Untitled

PhantomBlogger
Automated LLM-driven blog infrastructure for offensive asset management from a single Discord interface.
Overview • Architecture • Templates • Quick Start • Commands • Configuration • LLM Models
Developed during my work at InTheCyber Group
Overview
PhantomBlogger is a red team infrastructure tool that leverages local LLMs to generate realistic, niche-specific blog content at scale. It automates the full pipeline, from content generation to deployment, making it possible to maintain multiple credible-looking, content-driven websites with minimal manual effort.
The project is built around a single key principle: asset categorization takes time. Automated scanners, crawlers, and reputation systems need to observe a site publishing consistent, topical content over weeks or months before assigning it a legitimate category. PhantomBlogger handles this background work automatically, so sites are ready well before an engagement begins.
[!NOTE] 📖 Companion Blog Post
Technical write-up and project rationale: https://medium.com/inthecyber-posts/phantomblogger-automating-red-team-infrastructure-with-local-llms-b4f895908509
Architecture
PhantomBlogger uses a centralized back-end / lightweight VPS model. All heavy lifting (LLM inference, image retrieval, SSH management) runs on a single Manager machine. Each site VPS only needs to rebuild the static site when new content arrives.
Manager (Back-End)
A team-server or local machine that:
- Hosts the Discord bot — the primary operator interface
- Runs a local LLM via Ollama for article generation
- Fetches stock images from the Pixabay API
- Connects to each site via SSH/SFTP to upload posts and trigger builds
[!IMPORTANT] The Manager must be capable of running the llama3 model with Ollama. For GPU-accelerated inference, refer to Ollama's hardware requirements.
Site (VPS)
A lightweight VPS that:
- Hosts the static blog compiled by Eleventy (11ty)
- Serves content via Caddy with automatic HTTPS/Let's Encrypt
- Exposes a low-privilege
site-managerSSH account for remote management
No persistent process or API is required on the VPS. Caddy serves the static output from /opt/Blog-Template/dist and handles certificate renewal automatically.
Interaction Flow
flowchart LR
OP(["👤 Operator"])
subgraph MGR ["Manager (Team Server)"]
BOT["Discord Bot"]
LLM["Ollama LLM"]
PIX["Pixabay API"]
STG["LLM-OUT/\n(staging)"]
BOT --> LLM
BOT --> PIX
LLM --> STG
PIX --> STG
end
subgraph VPS ["Site VPS"]
SFTP["SFTP Upload"]
BUILD["npm run build\n(Eleventy)"]
DIST["/dist\n(static HTML)"]
CADDY["Caddy\n(HTTPS)"]
SFTP --> BUILD
BUILD --> DIST
CADDY -.- DIST
end
INET(["🌐 Internet"])
OP -->|"/gen /upload"| BOT
STG -->|"SSH/SFTP"| SFTP
INET -->|"HTTPS"| CADDY
Flow summary:
- Operator issues
/genwith a target site and content prompt - The bot reads site configuration (model, credentials, paths) from
config.json - The prompt is sent to Ollama — a structured Markdown article is returned
- A stock image is fetched from Pixabay and staged alongside the post
- On the next
/uploador auto-uploader cycle, files are transferred via SFTP - The Manager triggers
npm run buildremotely — Eleventy compiles Markdown to static HTML - Caddy serves the updated site immediately from
/opt/Blog-Template/dist
Templates
Three blog templates are included, all sharing a standardized provisioning interface. The setup script and Discord bot work with all of them without any modification.
| Template | Style | Best For |
|---|---|---|
Base-Template | Clean, minimal | General-purpose baseline; suitable for any niche |
Template-Ember | Warm, editorial | Health, lifestyle, and finance niches |
Template-Midnight | Dark, modern | Tech, security, and developer-focused content |
All templates use an identical data file structure (site.json, author.json, menu.json) and the same Eleventy post pipeline (posts.11tydata.js). Building a new niche variant is as simple as customizing those data files and selecting a different GEN_MODEL.
Quick Start
1. Set Up the Manager
Install dependencies, configure Ollama with the appropriate Modelfiles, and start the Discord bot on your team-server or local machine.
➡️ Manager Installation Instructions
2. Provision a Site VPS
Spin up a lightweight VPS with a domain pointing at it and run vps_setup.sh. The script handles all system setup, template deployment, and prints the JSON configuration snippet for the Manager.
➡️ Site Provisioning Instructions
3. Add the Site to config.json
Paste the JSON block from the provisioning script into the sites array in DiscordBOT/config.json. Set "enabled": true and fill in GEN_MODEL with the Ollama model name for this site's niche.
4. Interact via Discord
With the bot running and a site configured, all operations are handled through Discord slash commands.
Commands
All operations are performed through Discord slash commands. site parameters support autocomplete — start typing a name and Discord suggests matching options.
| Command | Parameters | Description |
|---|---|---|
/gen | site, prompt | Generate an article via LLM and fetch a stock image |
/upload | site | Upload staged posts/images to the VPS and trigger a rebuild |
/auto_uploader | state, poll_seconds | Start or stop the background upload loop |
/list | site | List all published articles on a remote site |
/delete | site, article | Delete a remote article and its image(s), then rebuild |
/test_sites | site (or all) | Run local + SSH connectivity diagnostics |
/logs | count (optional) | Retrieve the bot log file, newest entries first |
/uptime | — | Show bot uptime, start time, and PID |
➡️ Full Command Reference with screenshots
Configuration
The Manager is configured through DiscordBOT/config.json. A fully annotated example is available at DiscordBOT/config.example.json.
{
"generation_config": {
"GLOBAL_GEN_MODEL": "llama3-11ty",
"IMAGE_MODEL": "llama3-pixabay",
"OLLAMA_API_URL": "http://localhost:11434/api/generate",
"PIXABAY_API_KEY": "YOUR_PIXABAY_KEY",
"BASE_SITE_PATH": "/opt/PhantomBlogger/LLM-OUT/",
"HTTP_TIMEOUT_SEC": 300
},
"manager_config": {
"DISCORD_BOT_TOKEN": "YOUR_DISCORD_TOKEN",
"DISCORD_GUILD_ID": 123456789012345678,
"DISCORD_CHANNEL_ID": 123456789012345678,
"PYTHON_EXECUTABLE": "python3",
"GEN_SCRIPT_PATH": "/opt/PhantomBlogger/DiscordBOT/utils/gen_article.py",
"LOG_FILE": "/opt/PhantomBlogger/logs/discord-bot.log"
},
"sites": [
{
"name": "example.com",
"enabled": true,
"local_post_dir": "/opt/PhantomBlogger/LLM-OUT/example.com/posts/",
"local_image_dir": "/opt/PhantomBlogger/LLM-OUT/example.com/_images/",
"GEN_MODEL": "llama3-pharma-medical",
"remote": {
"host": "example.com",
"user": "site-manager",
"key": "BASE64_ENCODED_SSH_PRIVATE_KEY",
"key_passphrase": "",
"post_path": "/opt/Blog-Template/src/posts/",
"image_path": "/opt/Blog-Template/src/posts/img/",
"build_path": "/opt/Blog-Template/",
"build_cmd": "npm run build",
"static_site_location": "/opt/Blog-Template/dist"
}
}
]
}
Configuration field reference
generation_config
| Field | Description |
|---|---|
GLOBAL_GEN_MODEL | Default Ollama model used when a site has no GEN_MODEL override |
IMAGE_MODEL | Ollama model used to generate Pixabay image search terms |
OLLAMA_API_URL | Ollama REST API endpoint |
PIXABAY_API_KEY | API key for stock image retrieval |
BASE_SITE_PATH | Root directory for all local LLM output (LLM-OUT/) |
HTTP_TIMEOUT_SEC | Timeout for Ollama HTTP requests, in seconds |
manager_config
| Field | Description |
|---|---|
DISCORD_BOT_TOKEN | Discord bot token from the Developer Portal |
DISCORD_GUILD_ID | Server ID where slash commands are registered |
DISCORD_CHANNEL_ID | Channel ID where the bot posts generation/upload notifications |
PYTHON_EXECUTABLE | Python interpreter path (e.g. python3 or a venv path) |
GEN_SCRIPT_PATH | Absolute path to gen_article.py |
LOG_FILE | Absolute path for the bot log file |
sites[] entry
| Field | Description |
|---|---|
name | Site identifier used in Discord autocomplete |
enabled | Set false to disable without removing the entry |
local_post_dir | Local staging directory for generated .md files |
local_image_dir | Local staging directory for fetched images |
GEN_MODEL | Ollama model for this site's content niche (overrides GLOBAL_GEN_MODEL) |
remote.host | VPS hostname |
remote.user | SSH user (always site-manager) |
remote.key | Base64-encoded Ed25519 private key (generated by vps_setup.sh) |
remote.key_passphrase | Key passphrase, or empty string |
remote.post_path | Path to the Eleventy posts directory on the VPS |
remote.image_path | Path to the posts image directory on the VPS |
remote.build_path | Working directory for the remote build command |
remote.build_cmd | Build command (npm run build) |
remote.static_site_location | Path to the compiled static output served by Caddy |
LLM Models
Article generation uses Ollama with custom Modelfiles in LLM-CONF/. Each Modelfile wraps llama3 with a niche-specific system prompt that defines topic, tone, and output format. One example Modelfile is provided
| Model | Language | Niche |
|---|---|---|
llama3-pharma-medical | English | Pharmaceuticals, medicine, healthcare, biotechnology |
To add a model for a new niche, copy an existing Modelfile, update the SYSTEM prompt, and register it with Ollama:
ollama create my-niche-model -f /opt/PhantomBlogger/LLM-CONF/my-niche-model/Modelfile
Repository Structure
PhantomBlogger/
├── DiscordBOT/
│ ├── commands/ # Slash command modules
│ ├── utils/
│ │ └── gen_article.py # Article generation pipeline (Ollama → SFTP)
│ ├── main.py # Bot entry point
│ ├── requirements.txt
│ ├── config.example.json # Annotated configuration template
│ └── config.json # Live configuration (gitignored)
├── LLM-CONF/ # Ollama Modelfiles, one directory per niche model
├── LLM-OUT/ # Local staging area for generated content (gitignored)
├── Site/
│ ├── vps_setup.sh # Idempotent VPS provisioning script
│ ├── clean_template.sh # Strips build artifacts before template transfer
│ ├── Base-Template/ # Default blog template (Eleventy + Tailwind CSS)
│ ├── Template-Ember/ # Warm editorial template variant
│ ├── Template-Midnight/ # Dark modern template variant
│ └── Caddy/Caddyfile # Example Caddy configuration
├── logs/ # Bot log output (gitignored)
└── README.md