Threat Intelligence

Getting Started with OpenCTI: AlienVault OTX and Threat Landscape in One Compose File

TLT
Threat Landscape Team
2026-10-089 min read

Getting started with OpenCTI takes one Compose file: the platform, its dependencies, a worker, and two feed connectors. This tutorial stands that stack up locally and points it at AlienVault OTX and the Threat Landscape intelligence feed.

OpenCTI is an open-source threat-intelligence platform. It stores STIX 2.1 knowledge, and connectors are separate containers that pull from a feed and write bundles back in. The two feeds are complementary: OTX is a free, community-sourced baseline, while Threat Landscape is a vetted, analyst-produced intelligence feed. This setup uses Threat Landscape's intelligence feed, which delivers OSINT and darknet reports already linked to actors, malware, campaigns, intrusion sets, vulnerabilities, and attack patterns. That context is what turns raw indicators into a usable OpenCTI knowledge graph.

What you need

A machine with Docker Engine and the Compose plugin, plus about 16 GB of RAM. Docker Desktop is enough on macOS or Windows.

Elasticsearch needs a higher map count on Linux:

sudo sysctl -w vm.max_map_count=1048575

Add vm.max_map_count=1048575 to /etc/sysctl.conf if you want that to survive a reboot.

You also need two API keys before the connectors will ingest anything. The two feeds play different roles, so it is worth being deliberate about both.

AlienVault OTX is a free, community-sourced feed. Anyone can publish a pulse, so coverage is broad but variable in quality. It is a reasonable no-cost baseline. Create an account at otx.alienvault.com, open Settings, and copy the API key.

Threat Landscape is a vetted, analyst-produced feed. Instead of raw community submissions, it delivers OSINT and darknet reporting already linked to threat actors, malware, campaigns, intrusion sets, vulnerabilities, and attack patterns. That enrichment is what makes the data actionable rather than just voluminous. It is a commercial feed, and the connector enforces that: a free or Professional account is web-only and will be rejected.

  1. Create an account at threatlandscape.io.
  2. Subscribe to a plan that includes the Intelligence API and the OpenCTI connector. That is Team ($499/month, 30-day rolling window) or Enterprise (custom, full history). Professional ($49/month) does not include the API.
  3. Copy the API key issued to that account. The API accepts it as an apikey header or as a Bearer token. To rotate a key, write to [email protected].
  4. Keep the key for the environment file below. Do not commit it.

The connector calls https://api.threatlandscape.io/rest/v1. The feed value used here is intelligence, which is both OSINT and darknet. intelligence-osint and intelligence-darknet are the single-source variants. This tutorial does not use the IOC feed.

Create the project

mkdir opencti && cd opencti
uuidgen    # run twice: admin token and Threat Landscape connector id
openssl rand -base64 32

OPENCTI_ADMIN_TOKEN and each connector id must be a UUIDv4, or the platform container will not start.

Save this as .env. Replace the placeholders.

COMPOSE_PROJECT_NAME=opencti

MINIO_ROOT_USER=opencti
MINIO_ROOT_PASSWORD=change-me-minio
RABBITMQ_DEFAULT_USER=opencti
RABBITMQ_DEFAULT_PASS=change-me-rabbit
ELASTIC_MEMORY_SIZE=4G

OPENCTI_HOST=localhost
OPENCTI_PORT=8080
OPENCTI_EXTERNAL_SCHEME=http
[email protected]
OPENCTI_ADMIN_PASSWORD=ChangeMePlease
OPENCTI_ADMIN_TOKEN=00000000-0000-4000-8000-000000000001
OPENCTI_HEALTHCHECK_ACCESS_KEY=change-me-health
OPENCTI_ENCRYPTION_KEY=change-me-base64-key

CONNECTOR_OTX_ID=8bbae241-6289-4faf-b7d6-7503bed50bbc
OTX_API_KEY=paste-otx-key

CONNECTOR_THREATLANDSCAPE_ID=11111111-2222-4333-8444-555555555555
THREATLANDSCAPE_API_KEY=paste-team-or-enterprise-key

Write the Compose file

Save this as docker-compose.yml. It is a short form of the official stack in OpenCTI-Platform/docker: Redis, Elasticsearch, Silo for object storage, RabbitMQ, the platform, and one worker. The two connector services are appended at the bottom.

Connectors share the Compose network, so the platform URL inside a container is http://opencti:8080. The Threat Landscape service uses the variables from external-import/threatlandscape.

services:
  redis:
    image: redis:8.10.1
    restart: unless-stopped
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 10

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.19.21
    restart: unless-stopped
    volumes:
      - esdata:/usr/share/elasticsearch/data
    environment:
      - discovery.type=single-node
      - xpack.ml.enabled=false
      - xpack.security.enabled=false
      - thread_pool.search.queue_size=5000
      - ES_JAVA_OPTS=-Xms${ELASTIC_MEMORY_SIZE} -Xmx${ELASTIC_MEMORY_SIZE}
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:9200/_cluster/health || exit 1"]
      interval: 20s
      timeout: 10s
      retries: 15

  minio:
    image: pgsty/silo:latest
    restart: unless-stopped
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - s3data:/data
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    command: server /data --console-address ":9001"

  rabbitmq:
    image: rabbitmq:4.3-management
    restart: unless-stopped
    volumes:
      - amqpdata:/var/lib/rabbitmq
    environment:
      - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER}
      - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS}

  opencti:
    image: opencti/platform:latest
    restart: unless-stopped
    ports:
      - "${OPENCTI_PORT}:8080"
    depends_on:
      redis:
        condition: service_healthy
      elasticsearch:
        condition: service_healthy
    environment:
      - NODE_OPTIONS=--max-old-space-size=8096
      - APP__PORT=8080
      - APP__BASE_URL=${OPENCTI_EXTERNAL_SCHEME}://${OPENCTI_HOST}:${OPENCTI_PORT}
      - APP__ADMIN__EMAIL=${OPENCTI_ADMIN_EMAIL}
      - APP__ADMIN__PASSWORD=${OPENCTI_ADMIN_PASSWORD}
      - APP__ADMIN__TOKEN=${OPENCTI_ADMIN_TOKEN}
      - APP__ENCRYPTION_KEY=${OPENCTI_ENCRYPTION_KEY}
      - APP__HEALTH_ACCESS_KEY=${OPENCTI_HEALTHCHECK_ACCESS_KEY}
      - 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__PORT_MANAGEMENT=15672
      - RABBITMQ__MANAGEMENT_SSL=false
      - RABBITMQ__USERNAME=${RABBITMQ_DEFAULT_USER}
      - RABBITMQ__PASSWORD=${RABBITMQ_DEFAULT_PASS}
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/health?health_access_key=${OPENCTI_HEALTHCHECK_ACCESS_KEY}"]
      interval: 20s
      timeout: 10s
      retries: 20
      start_period: 120s

  worker:
    image: opencti/worker:latest
    restart: unless-stopped
    depends_on:
      opencti:
        condition: service_healthy
    environment:
      - OPENCTI_URL=http://opencti:8080
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - WORKER_LOG_LEVEL=info
    deploy:
      replicas: 1

  connector-alienvault:
    image: opencti/connector-alienvault:latest
    restart: unless-stopped
    depends_on:
      opencti:
        condition: service_healthy
    environment:
      - OPENCTI_URL=http://opencti:8080
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_OTX_ID}
      - CONNECTOR_NAME=AlienVault OTX
      - CONNECTOR_SCOPE=alienvault
      - CONNECTOR_LOG_LEVEL=info
      - CONNECTOR_DURATION_PERIOD=PT30M
      - ALIENVAULT_BASE_URL=https://otx.alienvault.com
      - ALIENVAULT_API_KEY=${OTX_API_KEY}
      - ALIENVAULT_TLP=White
      - ALIENVAULT_CREATE_OBSERVABLES=true
      - ALIENVAULT_CREATE_INDICATORS=true
      - ALIENVAULT_PULSE_START_TIMESTAMP=2024-01-01T00:00:00Z
      - ALIENVAULT_REPORT_TYPE=threat-report
      - ALIENVAULT_REPORT_STATUS=New
      - ALIENVAULT_GUESS_MALWARE=false
      - ALIENVAULT_GUESS_CVE=false
      - ALIENVAULT_DEFAULT_X_OPENCTI_SCORE=50

  connector-threatlandscape:
    image: opencti/connector-threatlandscape:latest
    restart: unless-stopped
    depends_on:
      opencti:
        condition: service_healthy
    environment:
      - OPENCTI_URL=http://opencti:8080
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_THREATLANDSCAPE_ID}
      - CONNECTOR_NAME=Threat Landscape
      - CONNECTOR_SCOPE=indicator,report,threat-actor,malware,campaign,intrusion-set,attack-pattern,vulnerability,identity,location
      - CONNECTOR_LOG_LEVEL=info
      - CONNECTOR_DURATION_PERIOD=PT1H
      - THREATLANDSCAPE_API_BASE_URL=https://api.threatlandscape.io/rest/v1
      - THREATLANDSCAPE_API_KEY=${THREATLANDSCAPE_API_KEY}
      - THREATLANDSCAPE_IMPORT_SINCE=P30D
      - THREATLANDSCAPE_FEED=intelligence
      - THREATLANDSCAPE_PAGE_SIZE=100

volumes:
  esdata:
  s3data:
  redisdata:
  amqpdata:

THREATLANDSCAPE_FEED=intelligence selects the combined intelligence feed. THREATLANDSCAPE_IMPORT_SINCE=P30D limits the first run to bundles published in the last 30 days. Later runs continue from the last sequence id, once an hour. A Team subscription only serves that 30-day window, so a longer lookback will not return older bundles. The OTX start timestamp is set to 2024 so the first pulse import stays small. The connector default of 2020 pulls a much larger backlog.

The platform image should be 6.8.12 or newer. That is the minimum the Threat Landscape connector requires.

Start the stack

docker compose up -d
docker compose ps
docker compose logs -f opencti

Elasticsearch has to go healthy before the platform starts its migration. Give it a few minutes on the first boot. When the log settles, open http://localhost:8080 and sign in with OPENCTI_ADMIN_EMAIL and OPENCTI_ADMIN_PASSWORD.

Confirm the feeds

Open Data, then Ingestion, then Connectors. Both AlienVault OTX and Threat Landscape should be registered. A connector with a last run is working. Use the refresh action on that page if you do not want to wait for the schedule: 30 minutes for OTX, one hour for Threat Landscape.

docker compose logs -f connector-alienvault
docker compose logs -f connector-threatlandscape

A 401 from Threat Landscape means the key is missing, or the account is Free or Professional. OTX pulses show up as reports, with indicators and observables. Threat Landscape bundles show up as reports linked to the objects in CONNECTOR_SCOPE.

What to do next

Pin opencti/platform and opencti/connector-threatlandscape to the same release once the lab is stable, instead of latest. Add the MITRE ATT&CK connector from the official Compose file before you try to map attack patterns onto techniques. Put TLS in front of port 8080 before anyone else can reach the instance. The admin token is a full API credential.


Bring the same structured intelligence into your own stack with the Threat Landscape Platform and start on the free account, no credit card required.

Ready to Transform Your Threat Intelligence?

See how Threat Landscape can reduce alert fatigue and improve your security operations