Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsTraining
StudyProjectsNewsletterHire MeAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Training
Study
Projects
Newsletter
Hire Me
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

1794+ Articles
149+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Checklists
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. Projects
  3. OpenCTI: Building a Cyber Threat Intelligence Platform in Your Homelab
OpenCTI: Building a Cyber Threat Intelligence Platform in Your Homelab
PROJECTIntermediate

OpenCTI: Building a Cyber Threat Intelligence Platform in Your Homelab

Deploy OpenCTI to aggregate, correlate, and visualize threat intelligence from MISP, NVD, AlienVault OTX, and Shodan — all in a self-hosted Docker stack.

Dylan H.

Projects

June 24, 2026
12 min read
3-5 hours

Tools & Technologies

DockerDocker ComposeOpenCTIElasticsearchRabbitMQMinIORedis

Overview

Threat intelligence is the practice of collecting, processing, and analyzing data about adversaries, their tactics, and indicators of compromise (IoCs) — then turning that raw information into actionable knowledge. Most organizations buy expensive commercial TIP (Threat Intelligence Platform) subscriptions; this project gives you a production-grade alternative for free.

OpenCTI (Open Cyber Threat Intelligence) is an open-source platform developed by Filigran and maintained in partnership with ANSSI (France's national cybersecurity agency). It models threat data using the STIX 2.1 standard, supports TAXII 2.1 feeds, and integrates with dozens of external sources via a connector ecosystem. The result is a searchable, graph-based knowledge base of threats, malware families, TTPs, vulnerabilities, and infrastructure.

In this project you will:

  • Deploy the full OpenCTI stack with Docker Compose (platform, Elasticsearch, Redis, RabbitMQ, MinIO)
  • Configure connectors for NVD/CVE, AlienVault OTX, MISP, and Shodan
  • Ingest threat data and explore relationships in the graph view
  • Integrate with Wazuh to enrich SIEM alerts with CTI context
  • Set up a TAXII server so downstream tools can pull live indicators

This pairs naturally with a Wazuh XDR/SIEM homelab and a Shuffle SOAR deployment — together they form a complete detect-enrich-respond pipeline.


Architecture

┌─────────────────────────────────────────────────────────────┐
│                     Docker Compose Stack                     │
│                                                             │
│  ┌──────────────┐    ┌──────────────┐   ┌───────────────┐  │
│  │   OpenCTI    │◄──►│Elasticsearch │   │    MinIO      │  │
│  │  :8080       │    │   :9200      │   │  (S3 store)   │  │
│  └──────┬───────┘    └──────────────┘   └───────────────┘  │
│         │                                                   │
│  ┌──────▼───────┐    ┌──────────────┐                      │
│  │   RabbitMQ   │    │    Redis     │                      │
│  │  :5672/:15672│    │   :6379      │                      │
│  └──────┬───────┘    └──────────────┘                      │
│         │                                                   │
│  ┌──────▼────────────────────────────────────────────────┐  │
│  │                  Connectors (workers)                  │  │
│  │   CVE/NVD │ AlienVault OTX │ MISP │ Shodan │ ...      │  │
│  └────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
          │                          │
   ┌──────▼──────┐          ┌────────▼──────┐
   │   Wazuh     │          │ Shuffle SOAR  │
   │ (SIEM/XDR)  │          │  (playbooks)  │
   └─────────────┘          └───────────────┘

OpenCTI uses RabbitMQ as its message bus — connectors push intelligence as STIX bundles onto the queue, and the platform workers consume and persist them into Elasticsearch. Files (malware samples, reports, attachments) are stored in MinIO (S3-compatible object storage). Redis handles caching and session data. The web UI exposes a GraphQL API consumed both by the frontend and by third-party integrations.

Resource Requirements

ComponentRAMDisk
OpenCTI platform1 GB—
Elasticsearch4–8 GB20 GB+
RabbitMQ512 MB1 GB
MinIO256 MB10 GB+
Redis256 MB—
Total recommended8 GB40 GB

A single VM or dedicated Docker host with 8 GB RAM and a 60 GB data volume is sufficient for a homelab deployment ingesting a few feeds.


Step 1: System Preparation

1.1 Elasticsearch kernel parameter

Elasticsearch requires a higher virtual memory map count than most Linux defaults. Set this permanently:

# Apply immediately
sudo sysctl -w vm.max_map_count=1048575
 
# Persist across reboots
echo "vm.max_map_count=1048575" | sudo tee -a /etc/sysctl.conf

Verify:

sysctl vm.max_map_count
# vm.max_map_count = 1048575

1.2 Generate UUIDs

OpenCTI requires several UUIDs for tokens and secrets. Generate them all at once:

for i in $(seq 1 6); do cat /proc/sys/kernel/random/uuid; done

Label and save these — you will use them in the .env file below.


Step 2: Docker Compose Setup

2.1 Create project directory

mkdir -p ~/opencti/connectors
cd ~/opencti

2.2 Environment file

Create .env (replace the UUIDs with your generated values):

# OpenCTI core
OPENCTI_ADMIN_EMAIL=admin@homelab.local
OPENCTI_ADMIN_PASSWORD=ChangeMe2026!
OPENCTI_ADMIN_TOKEN=<uuid-1>
OPENCTI_BASE_URL=http://localhost:8080
OPENCTI_HEALTHCHECK_ACCESS_KEY=<uuid-2>
 
# Elasticsearch
ELASTIC_MEMORY_SIZE=4G
 
# MinIO
MINIO_ROOT_USER=<uuid-3>
MINIO_ROOT_PASSWORD=<uuid-4>
 
# RabbitMQ
RABBITMQ_DEFAULT_USER=opencti
RABBITMQ_DEFAULT_PASS=<uuid-5>
 
# Connector secret (shared by all connectors)
CONNECTOR_EXPORT_FILE_STIX_ID=<uuid-6>
 
# Feed API keys (fill in later)
ALIENVAULT_API_KEY=
SHODAN_API_KEY=

2.3 Docker Compose file

Create docker-compose.yml:

version: "3.8"
 
services:
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
 
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.14.0
    restart: unless-stopped
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms${ELASTIC_MEMORY_SIZE} -Xmx${ELASTIC_MEMORY_SIZE}"
    volumes:
      - es_data:/usr/share/elasticsearch/data
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
 
  minio:
    image: minio/minio:latest
    restart: unless-stopped
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    command: server /data --console-address ":9001"
    volumes:
      - minio_data:/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3
 
  rabbitmq:
    image: rabbitmq:3.12-management-alpine
    restart: unless-stopped
    environment:
      RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    ports:
      - "15672:15672"  # Management UI (optional, remove in prod)
 
  opencti:
    image: opencti/platform:latest
    restart: unless-stopped
    environment:
      NODE_OPTIONS: --max-old-space-size=8096
      APP__PORT: 8080
      APP__BASE_URL: ${OPENCTI_BASE_URL}
      APP__ADMIN__EMAIL: ${OPENCTI_ADMIN_EMAIL}
      APP__ADMIN__PASSWORD: ${OPENCTI_ADMIN_PASSWORD}
      APP__ADMIN__TOKEN: ${OPENCTI_ADMIN_TOKEN}
      APP__APP_LOGS__LOGS_LEVEL: error
      REDIS__HOSTNAME: redis
      REDIS__PORT: 6379
      ELASTICSEARCH__URL: http://elasticsearch:9200
      MINIO__ENDPOINT: minio
      MINIO__PORT: 9000
      MINIO__USE_SSL: "false"
      MINIO__ACCESS_KEY: ${MINIO_ROOT_USER}
      MINIO__SECRET_KEY: ${MINIO_ROOT_PASSWORD}
      RABBITMQ__HOSTNAME: rabbitmq
      RABBITMQ__PORT: 5672
      RABBITMQ__USERNAME: ${RABBITMQ_DEFAULT_USER}
      RABBITMQ__PASSWORD: ${RABBITMQ_DEFAULT_PASS}
      SMTP__HOSTNAME: localhost
      SMTP__PORT: 25
    ports:
      - "8080:8080"
    depends_on:
      - redis
      - elasticsearch
      - minio
      - rabbitmq
 
volumes:
  redis_data:
  es_data:
  minio_data:
  rabbitmq_data:

2.4 Start the core stack

docker compose up -d
 
# Watch logs until OpenCTI reports it is listening
docker compose logs -f opencti
# Look for: "🚀 OpenCTI is now started"

Initial startup takes 3–5 minutes while Elasticsearch initialises its indices and OpenCTI runs migrations. Open http://localhost:8080 and log in with OPENCTI_ADMIN_EMAIL / OPENCTI_ADMIN_PASSWORD.


Step 3: Connectors

Connectors are standalone Docker containers that pull data from external sources and push STIX bundles to OpenCTI via RabbitMQ. Add them to a second compose file so you can manage them independently.

Create connectors/docker-compose.yml:

version: "3.8"
 
# Shared connector environment — avoids repeating connection params
x-connector-base: &connector-base
  networks:
    - opencti_default
  restart: unless-stopped
  environment: &connector-env
    OPENCTI_URL: http://opencti:8080
    OPENCTI_TOKEN: ${OPENCTI_ADMIN_TOKEN}
    CONNECTOR_CONFIDENCE_LEVEL: "75"
    CONNECTOR_LOG_LEVEL: error
 
networks:
  opencti_default:
    external: true
 
services:
 
  # ── CVE / NVD ──────────────────────────────────────────────
  connector-cve:
    <<: *connector-base
    image: opencti/connector-cve:latest
    environment:
      <<: *connector-env
      CONNECTOR_ID: "<uuid-cve>"          # generate: cat /proc/sys/kernel/random/uuid
      CONNECTOR_NAME: "Common Vulnerabilities and Exposures"
      CONNECTOR_SCOPE: identity,attack-pattern,course-of-action,vulnerability,x_opencti_asset_type
      CVE_BASE_URL: https://services.nvd.nist.gov/rest/json/cves/2.0
      CVE_ENABLE_EPSS: "true"
      CONNECTOR_RUN_AND_TERMINATE: "false"
      CONNECTOR_DURATION_PERIOD: PT6H     # sync every 6 hours
 
  # ── AlienVault OTX ─────────────────────────────────────────
  connector-alienvault:
    <<: *connector-base
    image: opencti/connector-alienvault:latest
    environment:
      <<: *connector-env
      CONNECTOR_ID: "<uuid-otx>"
      CONNECTOR_NAME: "AlienVault OTX"
      CONNECTOR_SCOPE: "alienvault"
      ALIENVAULT_BASE_URL: https://otx.alienvault.com
      ALIENVAULT_API_KEY: ${ALIENVAULT_API_KEY}
      ALIENVAULT_TLP: White
      ALIENVAULT_CREATE_OBSERVABLES: "true"
      ALIENVAULT_CREATE_INDICATORS: "true"
      ALIENVAULT_PULSE_START_TIMESTAMP: 2024-01-01T00:00:00      # start date
      ALIENVAULT_INTERVAL: 30                                     # minutes
 
  # ── MISP ───────────────────────────────────────────────────
  # Uncomment and configure if you have a MISP instance
  # connector-misp:
  #   <<: *connector-base
  #   image: opencti/connector-misp:latest
  #   environment:
  #     <<: *connector-env
  #     CONNECTOR_ID: "<uuid-misp>"
  #     CONNECTOR_NAME: "MISP"
  #     CONNECTOR_SCOPE: misp
  #     MISP_URL: http://misp.homelab.local
  #     MISP_REFERENCE_URL: http://misp.homelab.local
  #     MISP_KEY: <your-misp-api-key>
  #     MISP_SSL: "false"
  #     MISP_CREATE_REPORTS: "true"
  #     MISP_REPORT_TYPE: misp-event
  #     MISP_INTERVAL: 5
 
  # ── Shodan ─────────────────────────────────────────────────
  connector-shodan-internetdb:
    <<: *connector-base
    image: opencti/connector-shodan-internetdb:latest
    environment:
      <<: *connector-env
      CONNECTOR_ID: "<uuid-shodan>"
      CONNECTOR_NAME: "Shodan InternetDB"
      CONNECTOR_SCOPE: IPv4-Addr
      SHODAN_MAX_TLP: TLP:AMBER
      # Free Shodan InternetDB — no API key required

Generate a UUID for each connector:

for name in cve otx shodan; do
  echo "$name: $(cat /proc/sys/kernel/random/uuid)"
done

Start connectors:

cd connectors
docker compose up -d
 
# Verify connectors registered with OpenCTI
docker compose logs connector-cve --tail 20

In the OpenCTI UI navigate to Data → Connectors — you should see each connector listed as Connected.


Step 4: Get an AlienVault OTX API Key

AlienVault OTX is free and provides one of the richest public threat intelligence feeds:

  1. Register at otx.alienvault.com
  2. Navigate to Settings → API Integration
  3. Copy your OTX Key (a 64-character hex string)
  4. Add it to your .env:
    ALIENVAULT_API_KEY=<your-64-char-key>
    
  5. Restart the connector:
    cd connectors && docker compose restart connector-alienvault

Step 5: Exploring Threat Intelligence

5.1 Dashboard

After 10–15 minutes the CVE connector will start populating data. The OpenCTI dashboard shows:

  • Ingested entities — vulnerabilities, malware, threat actors, campaigns
  • Latest ingested — a timeline of recent additions
  • Relationship graph — linkages between entities

5.2 Vulnerability investigation

Search for a recent CVE (e.g., CVE-2025-32433):

  1. Go to Arsenal → Vulnerabilities
  2. Search by CVE ID
  3. Open the result — you will see CVSS score, EPSS probability, affected CPEs, and any linked threat actors or campaigns from OTX

5.3 Indicator search

Go to Observations → Indicators to browse IOCs (IPs, domains, hashes, URLs). Use the filter panel to narrow by:

  • valid_until > now — only active indicators
  • confidence >= 75 — high-confidence only
  • tlp = White — public intelligence

5.4 Knowledge graph

Open any entity and click the Knowledge tab, then Graph — this renders an interactive D3 graph of relationships. You can visually explore how a malware family relates to specific threat actors, campaigns, and targeted sectors.

5.5 TAXII server

OpenCTI includes a built-in TAXII 2.1 server. Navigate to Data → Sharing → TAXII Collections and create a collection filtered to your desired indicators. Other tools (Palo Alto, Wazuh, custom scripts) can then poll this endpoint to pull live IOCs:

https://<opencti-host>:8080/taxii2/root/collections/<collection-id>/objects/
Authorization: Bearer <OPENCTI_ADMIN_TOKEN>

Step 6: Wazuh Integration

Push OpenCTI indicators into Wazuh so SIEM alerts are automatically enriched with threat context.

6.1 Wazuh custom integration script

On your Wazuh manager, create /var/ossec/integrations/custom-opencti.py:

#!/usr/bin/env python3
"""Enrich Wazuh alerts with OpenCTI indicator lookups."""
 
import json
import sys
import urllib.request
import urllib.error
 
OPENCTI_URL = "http://opencti.homelab.local:8080"
OPENCTI_TOKEN = "YOUR_ADMIN_TOKEN"
 
GRAPHQL_QUERY = """
query IndicatorByValue($value: String!) {
  indicators(filters: {
    mode: and
    filters: [{ key: "value", values: [$value] }]
    filterGroups: []
  }) {
    edges {
      node {
        id
        name
        description
        confidence
        valid_until
        pattern
        x_opencti_main_observable_type
        killChainPhases { phase_name }
      }
    }
  }
}
"""
 
def lookup_indicator(value: str) -> dict | None:
    payload = json.dumps({"query": GRAPHQL_QUERY, "variables": {"value": value}}).encode()
    req = urllib.request.Request(
        f"{OPENCTI_URL}/graphql",
        data=payload,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {OPENCTI_TOKEN}",
        },
    )
    try:
        with urllib.request.urlopen(req, timeout=5) as resp:
            data = json.loads(resp.read())
            edges = data.get("data", {}).get("indicators", {}).get("edges", [])
            return edges[0]["node"] if edges else None
    except (urllib.error.URLError, KeyError, IndexError):
        return None
 
def main():
    alert = json.loads(sys.stdin.read())
    observables = [
        alert.get("data", {}).get("srcip"),
        alert.get("data", {}).get("dstip"),
        alert.get("data", {}).get("url"),
        alert.get("data", {}).get("md5"),
    ]
 
    for obs in filter(None, observables):
        indicator = lookup_indicator(obs)
        if indicator:
            print(json.dumps({
                "opencti": {
                    "matched_observable": obs,
                    "indicator_name": indicator["name"],
                    "confidence": indicator["confidence"],
                    "valid_until": indicator["valid_until"],
                    "kill_chain_phases": [p["phase_name"] for p in indicator.get("killChainPhases", [])],
                }
            }))
            return
 
    print(json.dumps({"opencti": {"matched_observable": None}}))
 
if __name__ == "__main__":
    main()

6.2 Register in ossec.conf

<integration>
  <name>custom-opencti</name>
  <hook_url>http://opencti.homelab.local:8080</hook_url>
  <level>3</level>
  <alert_format>json</alert_format>
</integration>

Restart Wazuh manager:

systemctl restart wazuh-manager

Wazuh alerts will now include opencti.* fields when an observable matches a known indicator.


Testing

Verify connector health

# In the core stack directory
docker compose ps
# All services should show "healthy" or "running"
 
# Check connector logs for successful pushes
docker compose -f connectors/docker-compose.yml logs connector-alienvault | grep -i "bundle\|push\|success"

API smoke test

curl -s -X POST http://localhost:8080/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${OPENCTI_ADMIN_TOKEN}" \
  -d '{"query": "{ about { version } }"}' | python3 -m json.tool

Expected output:

{
  "data": {
    "about": {
      "version": "6.x.x"
    }
  }
}

TAXII endpoint test

curl -s http://localhost:8080/taxii2/root/ \
  -H "Authorization: Bearer ${OPENCTI_ADMIN_TOKEN}" \
  -H "Accept: application/taxii+json;version=2.1" | python3 -m json.tool

Indicator count

After connectors have been running for 30+ minutes, check entity counts:

curl -s -X POST http://localhost:8080/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${OPENCTI_ADMIN_TOKEN}" \
  -d '{"query": "{ indicators { pageInfo { globalCount } } }"}' | python3 -m json.tool

A healthy OTX connector populates thousands of indicators within the first hour.


Deployment Notes

Reverse proxy with Traefik

If you have Traefik running in your homelab, add these labels to the opencti service:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.opencti.rule=Host(`cti.homelab.local`)"
  - "traefik.http.routers.opencti.entrypoints=websecure"
  - "traefik.http.routers.opencti.tls.certresolver=letsencrypt"
  - "traefik.http.services.opencti.loadbalancer.server.port=8080"

Backup strategy

The critical persistent volumes are es_data (all entity data) and minio_data (file attachments). Back these up with:

# Stop Elasticsearch before snapshotting to ensure consistency
docker stop elasticsearch
tar -czf opencti-backup-$(date +%Y%m%d).tar.gz \
  $(docker volume inspect opencti_es_data -f '{{.Mountpoint}}') \
  $(docker volume inspect opencti_minio_data -f '{{.Mountpoint}}')
docker start elasticsearch

Performance tuning

For larger feeds or a fleet environment, consider:

  • Increase ELASTIC_MEMORY_SIZE to 8G if you have the RAM
  • Add dedicated Elasticsearch data nodes (scale the ES service)
  • Run connectors on a separate Docker host to avoid competing for I/O with Elasticsearch

Extensions & Next Steps

More connectors to explore

ConnectorSourceValue
connector-mitreMITRE ATT&CKFull TTP framework
connector-urlscanurlscan.ioURL/domain intelligence
connector-virustotalVirusTotalHash and URL enrichment
connector-abuse-sslabuse.chSSL certificate IOCs
connector-feodotrackerabuse.chBotnet C2 IPs
connector-malwarebazaarabuse.chMalware samples + hashes
connector-hybrid-analysisHybrid AnalysisDynamic malware analysis

Add any of these by including a new service block in connectors/docker-compose.yml with a fresh UUID and the appropriate image/environment from the OpenCTI connector catalog.

Bi-directional MISP sync

If you deploy MISP alongside OpenCTI, configure connector-misp to pull MISP events into OpenCTI and connector-misp-export to push back enriched IoCs. This creates a closed-loop intelligence sharing workflow.

Threat hunting with GraphQL

OpenCTI's GraphQL API enables powerful automated queries. For example, identify all indicators related to ransomware groups active in the last 30 days:

{
  indicators(
    filters: {
      mode: and
      filters: [
        { key: "valid_until", values: ["now"], operator: gt }
        { key: "createdBy", values: ["ransomware"] }
      ]
      filterGroups: []
    }
  ) {
    edges {
      node { name pattern confidence valid_until }
    }
  }
}

Shuffle SOAR playbook

Wire OpenCTI's TAXII feed into a Shuffle workflow that:

  1. Polls the TAXII collection every 15 minutes for new high-confidence indicators
  2. Pushes them to Wazuh as custom threat rules
  3. Syncs them to Pi-hole as blocklist entries for C2 domains
  4. Posts a summary to a Discord channel

This gives you automated threat response without manual intervention — the defining feature of a mature SOC automation pipeline.

OpenCTI is one of the best freely available tools in the blue team arsenal. Once populated with a few weeks of feeds, your knowledge graph becomes a powerful investigative resource for any incident that lands in your SIEM.

#threat-intelligence#CTI#OpenCTI#STIX#MISP#Docker#Homelab#Security

Related Articles

Building a Wazuh XDR + SIEM Homelab

Deploy a full Wazuh stack in Docker to gain host-based intrusion detection, file integrity monitoring, vulnerability scanning, and active response across your…

14 min read

Runtime Security Monitoring with Falco: Detect Container

Deploy Falco on a Docker host to monitor container syscalls at the kernel level, write custom homelab detection rules, and route real-time alerts through.

12 min read

Self-Hosted Password Manager with Vaultwarden

Deploy a fully self-hosted, Bitwarden-compatible password manager using Vaultwarden on Docker with Caddy reverse proxy, automatic TLS, WebSocket...

10 min read
Back to all Projects