Adding Traefik Reverse Proxy to Opencti

By Adrian | April 28, 2020

Well this one was a bit of a learning experience for me. You see I have dabbled in the past with Traefik which seems to fit naturally when it comes to reverse proxy and Docker, but my efforts have come up short in the past through no fault but my own. Perhaps it was the fact I was trying to run before I could even crawl. Not to worry though. As i’ve picked up some more experience with Docker, reverse proxies and certificates over the years, things seemed to make a little more sense, but there was definitely a little RTFM required as well. I’m pretty happy that Traefik just works when it comes to the Lets Encrypt certificates as well.

I have had to tweak a few settings from the original post, but ill share my configuration.

Prerequisite Network for Traefik

In order to be able to create a reverse proxy with Traefik, you need to create a docker network. This can be done in Portainer, under Network, Add Networks. I have added a network named proxy that is configured as an overlay network. This network will need to be available by any service that requires the reverse proxy service.

portainer-add-network

I also used the following portainer-agent-stack.yml file. I have removed the GUI port mapping (9000:9000) from the ports section as this is mapped to the reverse proxy using a deployment label and will be presented when Traefik is up. You may need to have that GUI port mapping in while the stacks are configured.

version: '3.2'

services:
  agent:
    image: portainer/agent
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/lib/docker/volumes:/var/lib/docker/volumes
    networks:
      - agent_network
    deploy:
      mode: global
      placement:
        constraints: [node.platform.os == linux]

  portainer:
    image: portainer/portainer
    command: -H tcp://tasks.agent:9001 --tlsskipverify
    ports:
      - "18000:8000"
    volumes:
      - portainer_data:/data
    networks:
      - proxy
      - agent_network
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.role == manager]
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.portainer.entrypoints=https"
        - "traefik.http.routers.portainer.rule=Host(`portainer.example.com`)"
        - "traefik.http.routers.portainer.service=portainer"
        - "traefik.http.routers.portainer.tls=true"
        - "traefik.http.services.portainer.loadbalancer.server.port=9000"
        - "traefik.http.routers.portainer.tls.certresolver=mytlschallenge"

        - "traefik.http.routers.portainer_http.entrypoints=http"
        - "traefik.http.routers.portainer_http.rule=Host(`portainer.example.com`)"
        - "traefik.http.routers.portainer_http.middlewares=traefik-redirectscheme"
        - "traefik.http.middlewares.traefik-redirectscheme.redirectscheme.scheme=https"

networks:
  proxy:
    external: true
  agent_network:
    driver: overlay
    attachable: true

volumes:
  portainer_data:

Create Traefik stack

While in theory you could jam everything in the same stack file, I have created a Traefik stack in portainer. This way the Reverse Proxy function will just operate on it own, and you can use the labelling function in other stacks to reference start using it.

Within Portainer, select Stacks, Add Stack. Name the stack traefik and add the following yml into the editor.

version: "3.3"

services:
  traefik:
    image: traefik:v2.2
    environment:
      - AWS_HOSTED_ZONE_ID=${AWS_HOSTED_ZONE_ID}
      - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
      - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
    restart: always
    container_name: traefik
    ports:
      - "80:80"
      - "443:443"
    command:
      - --api.insecure=false
      - --api.dashboard=true
      - --api.debug=false
      - --log.level=DEBUG
      - --providers.docker=true
      - --providers.docker.swarmMode=true
      - --providers.docker.exposedbydefault=false
      - --providers.docker.network=proxy
      - --entrypoints.http.address=:80
      - --entrypoints.https.address=:443
      - --certificatesResolvers.mytlschallenge.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory
      - --certificatesResolvers.mytlschallenge.acme.dnsChallenge=true
      - --certificatesResolvers.mytlschallenge.acme.dnsChallenge.provider=route53
      - --certificatesresolvers.mytlschallenge.acme.email=youremail@example.com
      - --certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json

    volumes:
      - "letsencrypt:/letsencrypt"
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - proxy
    deploy:
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.api.entrypoints=https"
        - "traefik.http.routers.api.rule=Host(`traefik.example.com`)"
        - "traefik.http.routers.api.service=api@internal"
        - "traefik.http.routers.api.tls=true"
        - "traefik.http.services.api.loadbalancer.server.port=8080"
        - "traefik.http.routers.api.tls.certresolver=mytlschallenge"

        - "traefik.http.routers.api_http.entrypoints=http"
        - "traefik.http.routers.api_http.rule=Host(`traefik.example.com`)"
        - "traefik.http.routers.api_http.middlewares=traefik-redirectscheme"
        - "traefik.http.middlewares.traefik-redirectscheme.redirectscheme.scheme=https"
        - "traefik.http.routers.api.middlewares=auth"
        - "traefik.http.middlewares.auth.basicauth.users=<htpasswd generated>"

      placement:
        constraints: [node.role==manager]

networks:
  proxy:
    external: true

volumes:
  letsencrypt:
  

IMPORTANT: There are modifications you will need to make for your environment. These are as follows:

Setting What to do
certificatesResolvers.mytlschallenge.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory Lets Encrypt enforces strict limits when it comes to requesting certificates. Keep this configuration line in play until your certificate generation is working as expected. Your certificates will show as Fake LE and will have a valid timeframe, but will be invalid to the browser. It will show as Not Secure
certificatesresolvers.mytlschallenge.acme.email=youremail@example.com Use your own email address
“traefik.http.routers.api.rule=Host(`traefik.example.com`)” “traefik.http.routers.api_http.rule=Host(`traefik.example.com`)” Make sure whatever hostname you choose, you are able to ping it. This may mean creating a local entry in your hosts file.
“traefik.http.middlewares.auth.basicauth.users=<htpasswd generated>“ This entry can be generated with the following command:
echo $(htpasswd -nbB $USER “YourPasswordInHere”)

You will need to also create 3 Environment variables for AWS Route53. These variables are required for the Lets Encrypt DNS challenge and are added in the lower pane.

portainer-add-env-variables

Now when you create the stack, you should be able to access it on the hostname you entered. Traefik will redirect any request made to http across to https with this configuration.

Updating the OpenCTI Stack

As far as the configuration for the OpenCTI stack goes, there are added deployment labels for services that require the reverse proxy. In this case i’ve put opencti and minio on the reverse proxy and all the services are accessible on their own backend network. I have also added 1 extra environment variable as a cosmetic replacement for the OPENCTI_URL as its duplicated multiple times in the configuration.

Update the opencti stack with the following configuration.

version: '3'
services:
  grakn:
    image: graknlabs/grakn:1.6.2
    ports:
      - 48555:48555
    volumes:
      - grakndata:/grakn-core-all-linux/server/db
    networks:
      - backend
    restart: always
  redis:
    image: redis:5.0.8
    networks:
      - backend
    restart: always
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.6.2
    volumes:
      - esdata:/usr/share/elasticsearch/data
    environment:
      - discovery.type=single-node
    networks:
      - backend
    restart: always
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
  minio:
    image: minio/minio:RELEASE.2020-02-27T00-23-05Z
    volumes:
      - s3data:/data
    environment:
      MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
      MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
    networks:
      - proxy
      - backend
    command: server /data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3
    deploy:
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.minio.entrypoints=https"
        - "traefik.http.routers.minio.rule=Host(`minio.example.com`)"
        - "traefik.http.routers.minio.service=minio"
        - "traefik.http.routers.minio.tls=true"
        - "traefik.http.services.minio.loadbalancer.server.port=9000"
        - "traefik.http.routers.minio.tls.certresolver=mytlschallenge"

        - "traefik.http.routers.minio_http.entrypoints=http"
        - "traefik.http.routers.minio_http.rule=Host(`minio.example.com`)"
        - "traefik.http.routers.minio_http.middlewares=traefik-redirectscheme"
        - "traefik.http.middlewares.traefik-redirectscheme.redirectscheme.scheme=https"

    restart: always
  rabbitmq:
    image: rabbitmq:3.7-management
    environment:
      - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER}
      - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS}
    networks:
      - backend
    restart: always
  opencti:
    image: opencti/platform:3.1.0
    environment:
      - APP__PORT=8088
      - APP__ADMIN__EMAIL=${OPENCTI_ADMIN_EMAIL}
      - APP__ADMIN__PASSWORD=${OPENCTI_ADMIN_PASSWORD}
      - APP__ADMIN__TOKEN=${OPENCTI_ADMIN_TOKEN}
      - APP__LOGS_LEVEL=error
      - APP__LOGS=./logs
      - APP__REACTIVE=true
      - APP__COOKIE_SECURE=false
      - GRAKN__HOSTNAME=grakn
      - GRAKN__PORT=48555
      - GRAKN__TIMEOUT=30000
      - 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_ACCESS_KEY}
      - MINIO__SECRET_KEY=${MINIO_SECRET_KEY}
      - RABBITMQ__HOSTNAME=rabbitmq
      - RABBITMQ__PORT=5672
      - RABBITMQ__PORT_MANAGEMENT=15672
      - RABBITMQ__MANAGEMENT_SSL=false
      - RABBITMQ__USERNAME=${RABBITMQ_DEFAULT_USER}
      - RABBITMQ__PASSWORD=${RABBITMQ_DEFAULT_PASS}
      - PROVIDERS__LOCAL__STRATEGY=LocalStrategy
    depends_on:
      - grakn
      - redis
      - elasticsearch
      - minio
      - rabbitmq
    networks:
      - proxy
      - backend
    deploy:
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.opencti.entrypoints=https"
        - "traefik.http.routers.opencti.rule=Host(`opencti.example.com`)"
        - "traefik.http.routers.opencti.service=opencti"
        - "traefik.http.routers.opencti.tls=true"
        - "traefik.http.services.opencti.loadbalancer.server.port=8088"
        - "traefik.http.routers.opencti.tls.certresolver=mytlschallenge"

        - "traefik.http.routers.opencti_http.entrypoints=http"
        - "traefik.http.routers.opencti_http.rule=Host(`opencti.example.com`)"
        - "traefik.http.routers.opencti_http.middlewares=traefik-redirectscheme"
        - "traefik.http.middlewares.traefik-redirectscheme.redirectscheme.scheme=https"

    restart: always
  worker:
    image: opencti/worker:3.1.0
    environment:
      - OPENCTI_URL=${OPENCTI_ADMIN_URL}
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - WORKER_LOG_LEVEL=info
    networks:
      - backend
    depends_on:
      - opencti
    deploy:
      mode: replicated
      replicas: 3
    restart: always
  connector-export-file-stix:
    image: opencti/connector-export-file-stix:3.1.0
    environment:
      - OPENCTI_URL=${OPENCTI_ADMIN_URL}
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_EXPORT_FILE_STIX_ID} # Valid UUDv4
      - CONNECTOR_TYPE=INTERNAL_EXPORT_FILE
      - CONNECTOR_NAME=ExportFileStix2
      - CONNECTOR_SCOPE=application/json
      - CONNECTOR_CONFIDENCE_LEVEL=3
      - CONNECTOR_LOG_LEVEL=info
    networks:
      - backend
    restart: always
  connector-export-file-csv:
    image: opencti/connector-export-file-csv:3.1.0
    environment:
      - OPENCTI_URL=${OPENCTI_ADMIN_URL}
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_EXPORT_FILE_CSV_ID} # Valid UUDv4
      - CONNECTOR_TYPE=INTERNAL_EXPORT_FILE
      - CONNECTOR_NAME=ExportFileCsv
      - CONNECTOR_SCOPE=application/csv
      - CONNECTOR_CONFIDENCE_LEVEL=3
      - CONNECTOR_LOG_LEVEL=info
    networks:
      - backend
    restart: always
  connector-import-file-stix:
    image: opencti/connector-import-file-stix:3.1.0
    environment:
      - OPENCTI_URL=${OPENCTI_ADMIN_URL}
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_IMPORT_FILE_STIX_ID} # Valid UUDv4
      - CONNECTOR_TYPE=INTERNAL_IMPORT_FILE
      - CONNECTOR_NAME=ImportFileStix2
      - CONNECTOR_SCOPE=application/json
      - CONNECTOR_CONFIDENCE_LEVEL=3
      - CONNECTOR_LOG_LEVEL=info
    networks:
      - backend
    restart: always
  connector-import-file-pdf-observables:
    image: opencti/connector-import-file-pdf-observables:3.1.0
    environment:
      - OPENCTI_URL=${OPENCTI_ADMIN_URL}
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_IMPORT_FILE_PDF_OBSERVABLES_ID} # Valid UUDv4
      - CONNECTOR_TYPE=INTERNAL_IMPORT_FILE
      - CONNECTOR_NAME=ImportFilePdfObservables
      - CONNECTOR_SCOPE=application/pdf
      - CONNECTOR_CONFIDENCE_LEVEL=3
      - CONNECTOR_LOG_LEVEL=info
      - PDF_OBSERVABLES_CREATE_INDICATOR=False
    networks:
      - backend
    restart: always
  connector-opencti:
    image: opencti/connector-opencti:3.1.0
    environment:
      - OPENCTI_URL=${OPENCTI_ADMIN_URL}
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_OPENCTI_ID} # Valid UUDv4
      - CONNECTOR_TYPE=EXTERNAL_IMPORT
      - CONNECTOR_NAME=OpenCTI
      - CONNECTOR_SCOPE=identity,sector,region,country,city
      - CONNECTOR_CONFIDENCE_LEVEL=5
      - CONNECTOR_UPDATE_EXISTING_DATA=true
      - CONNECTOR_LOG_LEVEL=info
      - CONFIG_SECTORS_FILE_URL=https://raw.githubusercontent.com/OpenCTI-Platform/datasets/master/data/sectors.json
      - CONFIG_GEOGRAPHY_FILE_URL=https://raw.githubusercontent.com/OpenCTI-Platform/datasets/master/data/geography.json
      - CONFIG_INTERVAL=7 # Days
    networks:
      - backend
    restart: always
  connector-mitre:
    image: opencti/connector-mitre:3.1.0
    environment:
      - OPENCTI_URL=${OPENCTI_ADMIN_URL}
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_MITRE_ID} # Valid UUDv4
      - CONNECTOR_TYPE=EXTERNAL_IMPORT
      - CONNECTOR_NAME=MITRE ATT&CK
      - CONNECTOR_SCOPE=identity,attack-pattern,course-of-action,intrusion-set,malware,tool,report
      - CONNECTOR_CONFIDENCE_LEVEL=3
      - CONNECTOR_UPDATE_EXISTING_DATA=true
      - CONNECTOR_LOG_LEVEL=info
      - MITRE_ENTERPRISE_FILE_URL=https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json
      - MITRE_PRE_ATTACK_FILE_URL=https://raw.githubusercontent.com/mitre/cti/master/pre-attack/pre-attack.json
      - MITRE_INTERVAL=7 # Days
    networks:
      - backend
    restart: always
  connector-misp:
    image: opencti/connector-misp:3.1.0
    environment:
      - OPENCTI_URL=${OPENCTI_ADMIN_URL}
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_MISP_ID}
      - CONNECTOR_TYPE=EXTERNAL_IMPORT
      - CONNECTOR_NAME=MISP
      - CONNECTOR_SCOPE=misp
      - CONNECTOR_CONFIDENCE_LEVEL=3
      - CONNECTOR_UPDATE_EXISTING_DATA=false
      - CONNECTOR_LOG_LEVEL=info
      - MISP_URL=${CONNECTOR_MISP_URL} # Required
      - MISP_KEY=${CONNECTOR_MISP_API} # Required
      - MISP_SSL_VERIFY=False # Required
      - MISP_CREATE_REPORTS=True # Required, create report for MISP event
      - MISP_REPORT_CLASS=MISP Event # Optional, report_class if creating report for event
      - MISP_IMPORT_FROM_DATE=2000-01-01 # Optional, import all event from this date
      - MISP_IMPORT_TAGS=opencti:import,type:osint # Optional, list of tags used for import events
      - MISP_INTERVAL=1 # Required, in minutes
    networks:
      - backend
    restart: always
networks:
  backend:
  proxy:
    external: true
volumes:
  grakndata:
  esdata:
  s3data:

IMPORTANT: There are a few instances of example.com which you will need to change with your own domain.

What does it all mean

With all this new configuration, when the stacks come up Traefik will contact Lets Encrypt, create the DNS challenge in Route53, collect a certificate from Lets Encrypt and store it in the acme.json file (in the LetsEncrypt Volume). Sure beats having to setup each certificate manually and setup a cron job to update it every 3 months.

Testing

There are a lot of changes that were made from the previous post to this one. In the real world, make 1 change at a time, test as you go along. As far as testing went:

  • ensure you can access each of the services via https
  • ensure the certificates are signed by Lets Encrypt Authority X3 and not TRAEFIK SELF SIGNED or Fake LE
  • ensure you can login to each service
  • ensure that when you create and tag an indicator in MISP it syncs across to OpenCTI

lets-encrypt-urls

References

Heres a list of resources I found useful

Whats next

As far as this configuration goes, I wouldn’t mind trying to create an “all in one” docker-compose file that combined all the components I’ve used to date (Docker Swarm, Portainer, Traefik and OpenCTI) just to see if it could work.

With the OpenCTI infrastructure up, I also want to start getting into the nitty gritty of OpenCTI with some real world indicators.

Thanks for reading this far, I know it was a bit of a long post, but as with all my blog posts, I’m documenting this for my future self ;-)