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.
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.
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 notTRAEFIK SELF SIGNED
orFake LE
- ensure you can login to each service
- ensure that when you create and tag an indicator in MISP it syncs across to OpenCTI
References
Heres a list of resources I found useful
- Traefik Official Docs
- YouTube - How to use Traefik Reverse Proxy by Eficode Praqma
- Adding Basic Auth to Docker-Compose with Traefik
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 ;-)