Architecture, Foundation & Cross-VPS Networking
The multi-VPS foundation every other part of the series builds on. Network, identity, certificates, backups, and the webhook alerting baseline.
5 RamNode VPS, Ubuntu 24.04, sudo SSH
~60 minutes (excluding provisioning)
Hardened multi-zone foundation ready for T-Pot in Part 2
Why a Threat Intelligence Platform
A self-hosted threat intelligence platform pulls together four capabilities that are normally bought as separate products: deception (honeypots that capture attacker behaviour), network detection (signature and behavioural IDS), intelligence management (an IOC store with feed ingestion and distribution), and SIEM with response (correlation, alerting, and active blocking).
This series wires all four into a single coherent stack on commodity VPS infrastructure, using webhooks rather than email so it works inside RamNode's outbound-mail policy.
Stack Selection Rationale
Each component was chosen against credible alternatives:
- • T-Pot over a hand-rolled honeypot collection — pre-tuned Elastic stack, dozens of honeypot containers, dashboards out of the box.
- • Beelzebub over a second instance of Cowrie — captures LLM prompt-injection TTPs and MCP attacks that traditional honeypots cannot.
- • Suricata over Snort — multi-threaded by default, EVE JSON output that integrates cleanly with Wazuh.
- • Zeek alongside Suricata — behavioural logs and a scriptable intel framework that signature engines cannot replace.
- • MISP over OpenCTI — mature feed ecosystem, simple sharing model, native exports for Suricata and Zeek.
- • Wazuh over Security Onion — works as a distributed SIEM across heterogeneous VPSes without requiring SPAN ports.
Four-Zone Network Architecture
The combined resource footprint of T-Pot, MISP, and Wazuh exceeds what a single budget VPS can sustainably handle, and zone separation is itself a security control. The series uses four zones:
- • Deception zone — T-Pot host (16 GB / 200 GB) and Beelzebub host (2 GB / 40 GB). Internet-exposed by design.
- • Detection zone — Suricata + Zeek (4 GB / 80 GB) receiving mirrored traffic from deception and any monitored production assets.
- • Intelligence zone — MISP host (8 GB / 100 GB) for IOC management, feed ingestion, and downstream distribution.
- • Operations zone — Wazuh manager / indexer / dashboard (16 GB / 200 GB) plus a small management VPS (2 GB) running the bastion, the automation orchestrator, and the Caddy reverse proxy.
All zones connect through a WireGuard mesh; the management VPS is the only host that exposes SSH publicly.
VPS Sizing and Sample Bill of Materials
A reference deployment on RamNode looks like this:
Role Plan Approx. monthly
T-Pot Premium 16 GB / 200 GB ~$96
Wazuh stack Premium 16 GB / 200 GB ~$96
MISP Standard 8 GB / 100 GB ~$40
Suricata + Zeek Standard 4 GB / 80 GB ~$20
Beelzebub Standard 2 GB / 40 GB ~$10
Management/bastion Standard 2 GB / 40 GB ~$10
-------
Total ~$272/moYou can shave the bill by colocating Beelzebub on the management VPS, or by parking the detection zone on a 2 GB plan if you do not run Zeek with heavy scripts.
Hardened Ubuntu 24.04 Baseline
Apply the same baseline to every host in the stack before installing any component-specific software:
#!/usr/bin/env bash
set -euo pipefail
apt update && apt -y full-upgrade
apt install -y unattended-upgrades fail2ban ufw curl jq
dpkg-reconfigure -plow unattended-upgrades
# UFW defaults
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp comment 'ssh (replace with bastion-only later)'
ufw --force enable
# SSH hardening
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
systemctl restart ssh
# fail2ban: ssh by default; deception hosts will get extra jails in Part 2
systemctl enable --now fail2banOn the deception hosts, leave SSH on a non-standard port and behind WireGuard once the mesh is up.
WireGuard Mesh and Split-Horizon DNS
The mesh gives every component a stable internal IP independent of the public address. We use the 10.88.0.0/24 range:
[Interface]
PrivateKey = <mgmt-private>
Address = 10.88.0.1/24
ListenPort = 51820
[Peer] # tpot
PublicKey = <tpot-public>
AllowedIPs = 10.88.0.10/32
[Peer] # beelzebub
PublicKey = <bzb-public>
AllowedIPs = 10.88.0.11/32
[Peer] # detection
PublicKey = <det-public>
AllowedIPs = 10.88.0.20/32
[Peer] # misp
PublicKey = <misp-public>
AllowedIPs = 10.88.0.30/32
[Peer] # wazuh
PublicKey = <wazuh-public>
AllowedIPs = 10.88.0.40/32Run a tiny CoreDNS instance on the management VPS that resolves *.tip.internal against a static zonefile and forwards everything else to 1.1.1.1. Point all hosts at 10.88.0.1 for DNS.
Internal CA with step-ca for Service-to-Service TLS
Wazuh's internal indexer / manager / dashboard traffic, Caddy's upstream connections, and MISP's API all benefit from real TLS rather than self-signed sprawl. step-ca on the management VPS gives you a 90-day issuing CA with ACME support:
step ca init \
--name "TIP Internal" \
--dns ca.tip.internal \
--address :8443 \
--provisioner admin@tip.internal
systemctl enable --now step-caEvery host runs step ca bootstrap against the CA URL and uses the ACME provisioner with Caddy or the JWK provisioner with step ca certificate for non-HTTP services.
Caddy Bastion Reverse Proxy
All dashboards (Wazuh, MISP, T-Pot UI, Kibana) sit on internal addresses only. Caddy on the management VPS is the single public entrypoint with mutual TLS or Authelia in front of it:
{
acme_ca https://ca.tip.internal:8443/acme/acme/directory
}
wazuh.tip.example {
reverse_proxy 10.88.0.40:443 {
transport http { tls_insecure_skip_verify }
}
forward_auth authelia:9091 { uri /api/verify?rd=https://auth.tip.example }
}
misp.tip.example { reverse_proxy 10.88.0.30:443 }
tpot.tip.example { reverse_proxy 10.88.0.10:64297 }Centralised SSH Bastion
Configure every non-management VPS to accept SSH only from 10.88.0.1:
ufw delete allow 22/tcp
ufw allow from 10.88.0.1 to any port 22 proto tcpOn the bastion, use ~/.ssh/config with ProxyJump so day-to-day operators never need to learn the underlying IPs.
Restic Backup Strategy
Each zone backs up a small, well-defined set of paths to S3-compatible object storage. A typical schedule:
- • T-Pot —
/dataElastic snapshots nightly, keep 7 days. - • MISP — MariaDB dump +
/var/www/MISP/app/filesnightly, keep 30 days. - • Wazuh — indexer snapshots to S3 +
/var/ossec/etcdaily, keep 30 days. - • Detection — only configs and rules; PCAPs are intentionally ephemeral.
- • Management — step-ca state, Caddy config, WireGuard keys, automation scripts.
restic -r s3:s3.example.com/tip-backups backup \
/var/ossec/etc /etc/wazuh-indexer
restic -r s3:s3.example.com/tip-backups forget \
--keep-daily 30 --pruneWebhook Notification Baseline
Every component in this stack defaults to email. Because RamNode does not provide outbound mail relays, the series uses webhooks exclusively. Pick one notification sink (Slack, Discord, Matrix, or a generic HTTP receiver) and store its URL in a single shared secret that the rest of the series re-reads:
TIP_WEBHOOK_URL="https://hooks.slack.com/services/T000/B000/XXXX"
TIP_WEBHOOK_KIND="slack" # slack | discord | matrix | genericA 30-line shell wrapper at /usr/local/bin/tip-notify is enough to normalise messages across kinds; later parts call it from MISP webhook adapters, Wazuh integrations, and cron jobs.
Pre-Part-2 Validation Checklist
- •
wg showreports handshakes from every peer. - •
dig wazuh.tip.internal @10.88.0.1resolves on every host. - •
step ca healthreturns OK from all peers. - • Public SSH is closed everywhere except the bastion.
- •
tip-notify "foundation ready"hits your chosen sink. - •
restic snapshotsshows at least one snapshot from each host.
When all six tick, you are ready for Part 2: T-Pot Honeypot Platform Deployment.
