Threat Intelligence Platform Series
    Part 1 of 6

    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.

    60 minutes
    5 RamNode VPS
    Prerequisites

    5 RamNode VPS, Ubuntu 24.04, sudo SSH

    Time

    ~60 minutes (excluding provisioning)

    Outcome

    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/mo

    You 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:

    baseline.sh
    #!/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 fail2ban

    On 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:

    /etc/wireguard/wg0.conf (mgmt)
    [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/32

    Run 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-ca

    Every 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:

    Caddyfile
    {
      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 tcp

    On 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/data Elastic snapshots nightly, keep 7 days.
    • MISP — MariaDB dump + /var/www/MISP/app/files nightly, keep 30 days.
    • Wazuh — indexer snapshots to S3 + /var/ossec/etc daily, 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 --prune

    Webhook 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:

    /etc/tip/webhook.env
    TIP_WEBHOOK_URL="https://hooks.slack.com/services/T000/B000/XXXX"
    TIP_WEBHOOK_KIND="slack"   # slack | discord | matrix | generic

    A 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 show reports handshakes from every peer.
    • dig wazuh.tip.internal @10.88.0.1 resolves on every host.
    • step ca health returns OK from all peers.
    • • Public SSH is closed everywhere except the bastion.
    • tip-notify "foundation ready" hits your chosen sink.
    • restic snapshots shows at least one snapshot from each host.

    When all six tick, you are ready for Part 2: T-Pot Honeypot Platform Deployment.