×
🎓 Join the CrowdSec Academy: Level up on your cybersecurity knowledge
Start now
enhance docker compose security with crowdsec and traefik
Tutorial

Enhance Docker Compose Security with CrowdSec and Traefik Proxy

This is a guest post by community member, Killian Stein.

Hello everyone!

In this tutorial, I’m going to show you how to enhance your Docker security using CrowdSec and Traefik Proxy.

Before we get started, let's have a quick look at CrowdSec, a community-based security solution! CrowdSec analyzes attacks in real time, and provides access to a console that gives you detailed information on IPs, such as their activity rate, their danger score, and the types of attacks carried out on users within the CrowdSec network. 

Isn't that great? How have you been managing your security without analyzing data? Other solutions exist, but I challenge you to find a simpler and equally powerful one. 😉

Well, now that I got your attention, here's a diagram illustrating how CrowdSec works technically:

Source: https://docs.crowdsec.net/docs/intro

To sum up the illustration in a few words: You have data sources (e.g. reverse proxy connection logs), which are analyzed by the Log Processor that compares your logs to logs that include indications of attacks. The Local API retrieves the process analysis and then does several things:

  • LAPI makes a decision and informs the Remediation Component (bouncer) to enforce said decision
  • Shares information on malicious attacks with the CrowdSec community
  • Sends alerts on configured channels

Here's an overview of the information available in the CrowdSec Console — super handy!

Shall we move on to the lab?

Useful links

To follow along with this tutorial, here are a few links that will come in handy.

Setting up

For the demo, I'm going to use a small VPS (Ubuntu 23.04) from OVH. Here's the basic quick setup.

Note: Modify lines 15 and 16 if you want to secure your SSH port.


#!/bin/bash
echo "🚀 Let's Start to setup vps ! 🚀"
sudo apt update -y && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg lsb-release
sudo mkdir -m 0755 -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update -y
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo apt install -y iptables
sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
## Modify this line, after -s, indicate your ip public
##sudo iptables -A INPUT -p tcp --dport 22 -s Your_IP_Public -j ACCEPT
##sudo iptables -A INPUT -p tcp --dport 22 -j DROP
sudo -s iptables-save -c
sudo iptables -L --line-numbers
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
echo "🚀 Let's goooo ! 🚀"

And we're off!

I'm going to start with a basic configuration for Traefik Proxy. Another tutorial for Nginx Proxy Manager is already available, so I won't go into that here, but you can find the full CrowdSec + Nginx Proxy Manager tutorial here.

For this tutorial, I am going to use WordPress, Uptime Kuma, and a little Jenkins, so it'll speak to everyone. Here's the Docker Compose file — make sure to adapt it to your domain name.


version: '3'

services:
  traefik:
    restart: unless-stopped
    image: traefik:latest
    command:
      - --providers.docker=true
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.prodresolver.acme.email=youremail@email.com
      - --certificatesresolvers.prodresolver.acme.caserver=https://acme-v02.api.letsencrypt.org/directory
      - --certificatesresolvers.prodresolver.acme.keytype=RSA4096
      - --certificatesresolvers.prodresolver.acme.tlschallenge=true
      - --certificatesresolvers.prodresolver.acme.httpchallenge=true
      - --certificatesresolvers.prodresolver.acme.httpchallenge.entrypoint=web
      - --certificatesresolvers.prodresolver.acme.storage=/letsencrypt/acme.json
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - proxy
      - frontend

  # WordPress Service
  wordpress:
    image: wordpress:latest
    container_name: wordpress
    volumes:
      - wordpress_data:/var/www/html
      ## Traefik labels
    labels:
      - "traefik.enable=true"
      - traefik.http.routers.wordpress.rule=Host(`wordpress.yourdomain.com`)
      - traefik.http.routers.wordpress.tls=true
      - traefik.http.routers.wordpress.tls.certresolver=prodresolver
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DB_NAME: wordpress
    depends_on:
      - db
    restart: always
    networks:
      - frontend
      - backend

  # MySQL Service for WordPress
  db:
    image: mysql:8.0
    container_name: mysql
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: somewordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
    restart: always
    networks:
      - backend

  # Uptime Kuma Service
  uptime_kuma:
    image: louislam/uptime-kuma:latest
    container_name: uptime_kuma
    volumes:
      - uptime_kuma_data:/app/data
      ## Traefik labels
    labels:
      - "traefik.enable=true"
      - traefik.http.routers.uptime.rule=Host(`uptime.yourdomain.com`)
      - traefik.http.routers.uptime.tls=true
      - traefik.http.routers.uptime.tls.certresolver=prodresolver
    restart: always
    networks:
      - frontend
  # Jenkins Service
  jenkins:
    image: jenkins/jenkins:lts ## Traefik labels
    labels:
      - "traefik.enable=true"
      - traefik.http.routers.jenkins.rule=Host(`jenkins.yourdomain.com`)
      - traefik.http.routers.jenkins.tls=true
      - traefik.http.routers.jenkins.tls.certresolver=prodresolver
    volumes:
      - jenkins_data:/var/jenkins_home
    restart: always
    networks:
      - frontend
##Volumes part
volumes:
  wordpress_data:
  db_data:
  uptime_kuma_data:
  jenkins_data:

    ## Networks part
networks:
  proxy:
    external: true
  frontend:
    driver: bridge
  backend:
    driver: bridge
    

Before launching Docker Compose, I created the external network manually.


sudo docker network create proxy

Everything works!

Okay, we're now ready to integrate CrowdSec.

Integrating CrowdSec

By default, Crowdsec proposes some local dashboards through a Metabase Docker container, but it also provides a console managed at https://app.crowdsec.net.

For the sake of this tutorial, I'm only going to show you the CrowdSec Console.

Before adding the CrowdSec container, prepare the folder to host the logs and at the same time create a directory for the CrowdSec configuration.


sudo mkdir /var/log/crowdsec && sudo chown -R $USER:$USER /var/log/crowdsec
sudo mkdir /opt/crowdsec
sudo mkdir /opt/crowdsec-db

Next, simply add a CrowdSec container.


crowdsec:
    image: crowdsecurity/crowdsec
    container_name: crowdsec
    environment:
      PGID: "1000"
      COLLECTIONS: "crowdsecurity/traefik crowdsecurity/http-cve"
    expose:
      - "8080"
    volumes:
      - /var/log/crowdsec:/var/log/crowdsec:ro
      - /opt/crowdsec-db:/var/lib/crowdsec/data
      - /var/log/auth.log:/var/log/auth.log:ro
      - /opt/crowdsec:/etc/crowdsec
    restart: unless-stopped
    networks:
      - proxy

Okay, now that it's started, go to your CrowdSec Console to retrieve your enroll command.

Then use a docker exec to execute the command:


sudo docker exec crowdsec cscli console enroll XXXXX

Back to the Console to accept the enroll. 👀

Everything rolls, doesn't it? 😆

At this stage of the configuration, CrowdSec doesn't see the Traefik Proxy logs and no decision will be made, so you need to add a Remediation Component and the Traefik Proxy logs .

Note: Remediation Components are used to apply security measures, such as blocking malicious IP addresses. They act in response to signals and decisions taken by LAPI, which are based on log analysis performed by parsers.

So, let's generate a key for the Traefik Remediation Component.


sudo docker exec crowdsec cscli bouncers add traefik-bouncer
Start by preparing Traefik Proxy, mapping the previously created directory, and including the modifications in Traefik Proxy. You can take advantage of this change to customize log parameters and add the Remediation Component middleware:

traefik:
      restart: unless-stopped
      image: traefik:latest
      command:
        ## Logs for debugging
        - --log.filePath=/var/logs/traefik.log
        - --log.level=INFO # (Default: error) DEBUG, INFO, WARN, ERROR, FATAL, PANIC
        ## Logs for Crowdsec
        - --accessLog=true
        - --accessLog.filePath=/var/log/crowdsec/traefik.log
        - --accessLog.bufferingSize=100 # Configuring a buffer of 100 lines
        - --accessLog.filters.statusCodes=204-299,400-499,500-59 # Statut code to log
        - --providers.docker=true
        - --entrypoints.web.address=:80
        - --entrypoints.websecure.address=:443
        - --certificatesresolvers.prodresolver.acme.email=youremail@mail.fr
        - --certificatesresolvers.prodresolver.acme.caserver=https://acme-v02.api.letsencrypt.org/directory
        - --certificatesresolvers.prodresolver.acme.keytype=RSA4096
        - --certificatesresolvers.prodresolver.acme.tlschallenge=true
        - --certificatesresolvers.prodresolver.acme.httpchallenge=true
        - --certificatesresolvers.prodresolver.acme.httpchallenge.entrypoint=web
        - --certificatesresolvers.prodresolver.acme.storage=/letsencrypt/acme.json
        - "--entrypoints.http.http.middlewares=crowdsec-bouncer@docker"
        - "--entrypoints.https.http.middlewares=crowdsec-bouncer@docker"
      ports:
        - "80:80"
        - "443:443"
      volumes:
        - /var/log/crowdsec/:/var/log/crowdsec/
        - "./letsencrypt:/letsencrypt"
        - /var/run/docker.sock:/var/run/docker.sock:ro
      networks:
        - proxy
        - frontend

Top! Now, let’s raise the volume to CrowdSec level and create the Remediation Component in Docker Compose:


crowdsec:
    image: crowdsecurity/crowdsec
    container_name: crowdsec
    environment:
      PGID: "1000"
      COLLECTIONS: "crowdsecurity/traefik crowdsecurity/http-cve"
    expose:
      - "8080"
    volumes:
      - /var/log/crowdsec:/var/log/crowdsec:ro
      - /opt/crowdsec-db:/var/lib/crowdsec/data
      - /var/log/auth.log:/var/log/auth.log:ro
      - /opt/crowdsec:/etc/crowdsec
    restart: unless-stopped
    networks:
      - proxy
    ## Boncer service
  crowdsec-traefik-bouncer:
    image: fbonalair/traefik-crowdsec-bouncer
    container_name: bouncer-traefik
    environment:
      CROWDSEC_BOUNCER_API_KEY: ## Your API Key
      CROWDSEC_AGENT_HOST: crowdsec:8080
      GIN_MODE: release
    expose:
      - "8080"
    depends_on:
      - crowdsec
    restart: unless-stopped
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.middlewares.crowdsec-bouncer.forwardauth.address=http://bouncer-traefik:8080/api/v1/forwardAuth"
      - "traefik.http.middlewares.crowdsec-bouncer.forwardauth.trustForwardHeader=true"
      - "traefik.http.services.crowdsec-bouncer.loadbalancer.server.port=8080"
      

Modify the Crowdsec acquisition file which allows it to specify which logs to parse.

Path to file is /opt/crowdsec/acquis.yaml.


---
filenames:
  - /var/log/crowdsec/traefik.log
labels:
  type: traefik
  

Here is the complete Docker Compose file:


version: '3'


services:
  # Crowdsec Service
  crowdsec:
    image: crowdsecurity/crowdsec
    container_name: crowdsec
    environment:
      PGID: "1000"
      COLLECTIONS: "crowdsecurity/traefik crowdsecurity/http-cve"
    expose:
      - "8080"
    volumes:
      - /var/log/crowdsec:/var/log/crowdsec:ro
      - /opt/crowdsec-db:/var/lib/crowdsec/data
      - /var/log/auth.log:/var/log/auth.log:ro
      - /opt/crowdsec:/etc/crowdsec
    restart: unless-stopped
    networks:
      - proxy
    ## Boncer service
  crowdsec-traefik-bouncer:
    image: fbonalair/traefik-crowdsec-bouncer
    container_name: bouncer-traefik
    environment:
      CROWDSEC_BOUNCER_API_KEY: ## Your API Key
      CROWDSEC_AGENT_HOST: crowdsec:8080
      GIN_MODE: release
    expose:
      - "8080"
    depends_on:
      - crowdsec
    restart: unless-stopped
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.middlewares.crowdsec-bouncer.forwardauth.address=http://bouncer-traefik:8080/api/v1/forwardAuth"
      - "traefik.http.middlewares.crowdsec-bouncer.forwardauth.trustForwardHeader=true"
      - "traefik.http.services.crowdsec-bouncer.loadbalancer.server.port=8080"
  # Traefik Service
  traefik:
    restart: unless-stopped
    image: traefik:latest
    command:
      ## Logs for debugging
      - --log.filePath=/var/logs/traefik.log
      - --log.level=INFO # (Default: error) DEBUG, INFO, WARN, ERROR, FATAL, PANIC
      ## Logs for Crowdsec
      - --accessLog=true
      - --accessLog.filePath=/var/log/crowdsec/traefik.log
      - --accessLog.bufferingSize=100 # Configuring a buffer of 100 lines
      - --accessLog.filters.statusCodes=204-299,400-499,500-59 # Statut code to log
      - --providers.docker=true
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.prodresolver.acme.email=youremail@mail.fr
      - --certificatesresolvers.prodresolver.acme.caserver=https://acme-v02.api.letsencrypt.org/directory
      - --certificatesresolvers.prodresolver.acme.keytype=RSA4096
      - --certificatesresolvers.prodresolver.acme.tlschallenge=true
      - --certificatesresolvers.prodresolver.acme.httpchallenge=true
      - --certificatesresolvers.prodresolver.acme.httpchallenge.entrypoint=web
      - --certificatesresolvers.prodresolver.acme.storage=/letsencrypt/acme.json
      - "--entrypoints.http.http.middlewares=crowdsec-bouncer@docker"
      - "--entrypoints.https.http.middlewares=crowdsec-bouncer@docker"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/log/crowdsec/:/var/log/crowdsec/
      - "./letsencrypt:/letsencrypt"
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - proxy
      - frontend


  # WordPress Service
  wordpress:
    image: wordpress:latest
    container_name: wordpress
    volumes:
      - wordpress_data:/var/www/html
      ## Traefik labels
    labels:
      - "traefik.enable=true"
      - traefik.http.routers.wordpress.rule=Host(`wordpress.yourdomain.com`)
      - traefik.http.routers.wordpress.tls=true
      - traefik.http.routers.wordpress.tls.certresolver=prodresolver
      # Create bouncer middleware
      - "traefik.http.middlewares.crowdsec-bouncer.forwardauth.address=http://bouncer-traefik:8080/api/v1/forwardAuth"
      - "traefik.http.middlewares.crowdsec-bouncer.forwardauth.trustForwardHeader=true"
      - traefik.http.routers.wordpress.middlewares=crowdsec-bouncer@docker
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DB_NAME: wordpress
    depends_on:
      - db
    restart: always
    networks:
      - frontend
      - backend


  # MySQL Service for WordPress
  db:
    image: mysql:8.0
    container_name: mysql
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: somewordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
    restart: always
    networks:
      - backend


  # Uptime Kuma Service
  uptime_kuma:
    image: louislam/uptime-kuma:latest
    container_name: uptime_kuma
    volumes:
      - uptime_kuma_data:/app/data
      ## Traefik labels
    labels:
      - "traefik.enable=true"
      - traefik.http.routers.uptime.rule=Host(`uptime.yourdomain.com`)
      - traefik.http.routers.uptime.tls=true
      - traefik.http.routers.uptime.tls.certresolver=prodresolver
      # Create bouncer middleware
      - "traefik.http.middlewares.crowdsec-bouncer.forwardauth.address=http://bouncer-traefik:8080/api/v1/forwardAuth"
      - "traefik.http.middlewares.crowdsec-bouncer.forwardauth.trustForwardHeader=true"
      - traefik.http.routers.uptime.middlewares=crowdsec-bouncer@docker
    restart: always
    networks:
      - frontend
  # Jenkins Service
  jenkins:
    image: jenkins/jenkins:lts ## Traefik labels
    labels:
      - "traefik.enable=true"
      - traefik.http.routers.jenkins.rule=Host(`jenkins.yourdomain.com`)
      - traefik.http.routers.jenkins.tls=true
      - traefik.http.routers.jenkins.tls.certresolver=prodresolver
      # Create bouncer middleware
      - "traefik.http.middlewares.crowdsec-bouncer.forwardauth.address=http://bouncer-traefik:8080/api/v1/forwardAuth"
      - "traefik.http.middlewares.crowdsec-bouncer.forwardauth.trustForwardHeader=true"
      - traefik.http.routers.jenkins.middlewares=crowdsec-bouncer@docker
    volumes:
      - jenkins_data:/var/jenkins_home
    restart: always
    networks:
      - frontend
##Volumes part
volumes:
  wordpress_data:
  db_data:
  uptime_kuma_data:
  jenkins_data:


    ## Networks part
networks:
  proxy:
    external: true
  frontend:
    driver: bridge
  backend:
    driver: bridge
    

To verify that everything's OK and that public IPs have been transferred, use the Docker logs.


sudo docker logs crowdsec

Note: If you don't retrieve the public IP, remember to use echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf.

The CrowdSec Console checks that the bouncer is present, and I'll do a scan with Nikto to see if the bouncer is active.

The results are available after a few seconds.


sudo docker exec crowdsec cscli decisions list

And here are the alerts inside the CrowdSec Console:

Now, if you try to get to jenkins.ninapepite.ovh you will be denied access.

Not bad, eh?

We have several alerts on the same IP, you might to tell me, but we can still make attacks? No! The network packet flow explains why:

Source: https://blog.levassb.ovh/post/crowdsec/

The packet is received, marked as malicious, an alert is generated, then returns a 403 error to the attacker. Wonderful!

Note: To unban your IP use the following:


sudo docker exec crowdsec cscli decisions delete -i X.X.X 

Setting up alerts

So far so good, but now I'd like to be alerted without having to go to the CrowdSec Console. Phone notifications are great for production, aren't they?

For this tutorial, I’m going to use Slack, but you can connect to any other application of this kind. You can find detailed instructions in the CrowdSec documentation

Make a small edit to the /opt/crowdsec/notifications/slack.yaml file and you'll see how the CrowdSec team made our job easier!

All you have to do now is indicate the URL.

Next, tell the ban profile to send a notification about our slack_default configuration. Again, you need to edit the /opt/crowdsec/profiles.yaml file.

Note: The profile is the link between the alert and the notification. As soon as an alert matches the profile filter, a notification will be sent to the indicated source.

Now let’s restart CrowdSec.

Important note: Be extra careful with the scans. After testing on various platforms, my mobile operator's IP doesn't score very well on CrowdSec.

After the scan, the notifications came in.

Setting up allowlists

Allowlists can be very useful, especially in production. If something goes wrong with your internal connections, you'll have a way of mitigating it quickly.

Let's create a whitelist_custom.yaml file in /opt/crowdsec/parsers/s02-enrich/whitelists_custom.yaml.


##https://app.crowdsec.net/hub/author/crowdsecurity/configurations/whitelists
name: crowdsecurity/whitelists
description: "Whitelist events from private ipv4 addresses"
whitelist:
 reason: "private ipv4/ipv6 ip/ranges"
 ip:
   - "127.0.0.1"
   - "::1"
 cidr:
   - "192.168.0.0/16"
   - "10.0.0.0/8"
   - "172.16.0.0/12"
   

Let’s restart Docker and test it!

After the restart, if I do a scan, I won't be banned.

Last but not least — blocklists

I've got one last thing to show you, the blocklists. Just take a look at this. 👀

A dynamic fail2ban →

As you can see, in just a few clicks, you can ban thousands of malicious IPs in seconds, and for free! It's better than searching for blocklists on GitHub, isn't it? 😆

Last tips

In this article, I haven't explored the commands available via the CrowdSec CLI. I recommend you take a look at the documentation, which will give you an overview of all the available commands. 

Here's a shortcut: The cscli metrics command is very useful for getting an overview of your CrowdSec components and also for checking which logs are being processed, which is quite handy when troubleshooting.

That's all for this article!

I hope this tutorial provided you with a helpful overview of the capabilities of the CrowdSec suite. Personally, I install it on all my configurations and find it very reassuring to have so much information on the security status of my machine.

Let's not forget to thank the CrowdSec team for this complete solution! ❤️

Thanks for reading, and see you soon!

About Killian Stein

As a young IT enthusiast and teaching enthusiast, Killian tries to demystify modern technologies.
He is a DevSecOps Engineer at Aidalinfo, where he is learning and consolidating his experience of the cloud and open source tools. If you liked Killian’s article and are curious to follow his next projects, don't hesitate to connect with him on LinkedIn. New projects are coming soon! 

No items found.