Skip to main content

Traefik Crowdsec Stack einrichten

Der traefik-crowdsec-stack soll einen sicheren Betrieb von Docker Services ermöglichen. Dabei wird als Reverse-Proxy und Load-Balancer die derzeit aktuellste Version 3.x.x von Traefik eingesetzt. Die Kommunikation der von Traefik gerouteten Services wird  mit Crowdsec in eigenen Netzwerken abgesichert. Schließlich wird der Stack um die von Außen erreichbaren Dienste "Ralfs Blogs", "Bines Blog", "Bücherregal" und "Gitea-Server" erweitert. Alle hierzu benötigten Docker Services können bequem über eine einzelne Docker Compose Datei gesteuert werden.

Die Basis für die hier verwendeten Texte und den Code bildet der Artikel Traefik V3 Installation, Konfiguration und CrowdSec-Security von

Einrichten der grundlegenden Stack-Struktur

Bevor es richtig losgeht noch ein wichtiger Hinweis: Alle Shell-Befehle sind mit root Privilegien auszuführen. In den Skripten wird kein vorangestelltes sudo verwendet. Das Skript selbst ist mit sudo aufzurufen.

Um den Stack anzulegen, wird das Hauptverzeichnis traefik-crowdsec-stack unter /opt/containers/ erstellt und in dieses Verzeichnis gewechselt. Danach werden die erforderlichen Unterverzeichnisse und Dateien erzeugt, um den Stack mit crowdsec, socket-proxy und traefik zu konfigurieren. Schließlich werden noch die korrekten Berechtigungen für sensible Zertifikatsdateien gesetzt.

Anschließend werden die in der Struktur angelegten Konfigurations- und Environmentdateien mit Inhalten gefüllt. Dabei werden Parameter wie Passwörter oder API-Schlüssel mit einem in doppelt geschweiften Klammern versehenem Kommentar versehen. Diese Parameter sind im Nachgang von Hand anzupassen.

Um den Stack vorzubereiten, kann das nachfolgende Skript ausgeführt werden (zum Aufklappen auf den Namen klicken):

create_stack.sh
#!/bin/bash
#
# ensure that user has root privileges
#
if [ $(id -u) -ne 0 ]
  then echo Please run this script as root or using sudo!
  exit
fi

#
# create traefic-crowdsec-stack directory structure
#
mkdir -p /opt/containers/traefik-crowdsec-stack && cd /opt/containers/traefik-crowdsec-stack
mkdir -p compose data/{crowdsec/{config,data},socket-proxy,traefik/{certs,dynamic_conf},traefik-crowdsec-bouncer,service-blog/{bine,ralf},service-bookstack/{app,db},service-gitea}

#
# Create some files and change permissions of cert related files
#
touch .env docker-compose.yml \
compose/{crowdsec.yml,networks.yml,socket-proxy.yml,traefik-crowdsec-bouncer.yml,traefik.yml} \
data/{crowdsec/.env,socket-proxy/.env,traefik/{.env,.htpasswd,traefik.yml,certs/{acme_letsencrypt.json,tls_letsencrypt.json},dynamic_conf/{http.middlewares.default.yml,http.middlewares.default-security-headers.yml,http.middlewares.gzip.yml,http.middlewares.traefik-bouncer.yml,http.middlewares.traefik-dashboard-auth.yml,tls.yml}},traefik-crowdsec-bouncer/.env}

chmod 600 data/traefik/certs/{acme_letsencrypt.json,tls_letsencrypt.json}

#
# Fill master dotenv file
#
cat << 'EOF' > /opt/containers/traefik-crowdsec-stack/.env
ABSOLUTE_PATH=${PWD}
TZ=Europe/Berlin
SERVICES_TRAEFIK_LABELS_TRAEFIK_HOST=HOST(`traefik.rastef.de`)
EOF

#
# Fill master compose file
#
echo "include:
  - compose/service-blog-bine.yml
  - compose/service-blog-ralf.yml
  - compose/service-bookstack.yml
  - compose/service-gitea.yml
  - compose/crowdsec.yml
  - compose/socket-proxy.yml
  - compose/traefik-crowdsec-bouncer.yml
  - compose/traefik.yml
  - compose/networks.yml" >> /opt/containers/traefik-crowdsec-stack/docker-compose.yml


#
# Fill Crowdsec compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/crowdsec.yml
services:
  crowdsec:
    container_name: ${SERVICES_CROWDSEC_CONTAINER_NAME:-crowdsec}
    env_file: ${ABSOLUTE_PATH}/data/crowdsec/.env
    hostname: ${SERVICES_CROWDSEC_HOSTNAME:-crowdsec}
    healthcheck:
      test: ["CMD", "cscli", "version"]
      timeout: 2s
      interval: 20s
      retries: 5
      start_period: 10s
    image: ${SERVICES_CROWDSEC_IMAGE:-crowdsecurity/crowdsec}:${SERVICES_CROWDSEC_IMAGE_VERSION:-latest}
    networks:
      crowdsec:
        ipv4_address: ${SERVICES_CROWDSEC_NETWORKS_CROWDSEC_IPV4:-172.31.127.254}
        ipv6_address: ${SERVICES_CROWDSEC_NETWORKS_CROWDSEC_IPV6:-fd00:1:be:a:7001:0:3e:6fff}
      socket_proxy:
        ipv4_address: ${SERVICES_TRAEFIK_NETWORKS_SOCKET_PROXY_IPV4:-172.31.255.252}
        ipv6_address: ${SERVICES_TRAEFIK_NETWORKS_SOCKET_PROXY_IPV6:-fd00:1:be:a:7001:0:3e:8ffd}
    restart: unless-stopped
    security_opt:
      - no-new-privileges=true
    volumes:
      - ${ABSOLUTE_PATH}/data/crowdsec/config:/etc/crowdsec
      - ${ABSOLUTE_PATH}/data/crowdsec/data:/var/lib/crowdsec/data
      - /var/log/auth.log:/var/log/auth.log:ro
      - /var/log/traefik:/var/log/traefik:ro
EOF

#
# Fill Socket-Proxy compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/socket-proxy.yml
services:
  socket-proxy:
    container_name: ${SERVICES_SOCKET_PROXY_CONTAINER_NAME:-socket-proxy}
    env_file: ${ABSOLUTE_PATH}/data/socket-proxy/.env
    hostname: ${SERVICES_SOCKET_PROXY_HOSTNAME:-socket-proxy}
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider http://localhost:2375/_ping || exit 1"]
      timeout: 3s
      interval: 20s
      retries: 3
      start_period: 10s
    image: ${SERVICES_SOCKET_PROXY_IMAGE:-lscr.io/linuxserver/socket-proxy}:${SERVICES_SOCKET_PROXY_IMAGE_VERSION:-latest}
    networks:
      socket_proxy:
        ipv4_address: ${SERVICES_SOCKET_PROXY_NETWORKS_SOCKET_PROXY_IPV4:-172.31.255.254}
        ipv6_address: ${SERVICES_SOCKET_PROXY_NETWORKS_SOCKET_PROXY_IPV6:-fd00:1:be:a:7001:0:3e:8fff}
    read_only: true
    restart: unless-stopped
    tmpfs:
      - /run
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
EOF

#
# Fill Traefik Crowdsec Bouncer compose file
#
cat << 'EOF' > /opt/containers/traefik-crowdsec-stack/compose/traefik-crowdsec-bouncer.yml
services:
  traefik_crowdsec_bouncer:
    container_name: ${SERVICES_TRAEFIK_CROWDSEC_BOUNCER_CONTAINER_NAME:-traefik_crowdsec_bouncer}
    depends_on:
      crowdsec:
        condition: service_healthy
    env_file: ${ABSOLUTE_PATH}/data/traefik-crowdsec-bouncer/.env
    hostname: ${SERVICES_TRAEFIK_CROWDSEC_BOUNCER_HOSTNAME:-traefik-crowdsec-bouncer}
    image: ${SERVICES_TRAEFIK_CROWDSEC_BOUNCER_IMAGE:-ghcr.io/freifunkmuc/traefik-crowdsec-bouncer}:${SERVICES_TRAEFIK_CROWDSEC_BOUNCER_IMAGE_VERSION:-latest}
    networks:
      crowdsec:
        ipv4_address: ${SERVICES_TRAEFIK_CROWDSEC_BOUNCER_NETWORKS_CROWDSEC_IPV4:-172.31.127.252}
        ipv6_address: ${SERVICES_TRAEFIK_CROWDSEC_BOUNCER_NETWORKS_CROWDSEC_IPV6:-fd00:1:be:a:7001:0:3e:6ffd}
    restart: unless-stopped
EOF

#
# Fill Traefik compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/traefik.yml
services:
  traefik:
    container_name: ${SERVICES_TRAEFIK_CONTAINER_NAME:-traefik}
    depends_on:
      crowdsec:
        condition: service_healthy
      socket-proxy:
        condition: service_healthy
    env_file: ${ABSOLUTE_PATH}/data/traefik/.env
    hostname: ${SERVICES_TRAEFIK_HOSTNAME:-traefik}
    healthcheck:
      test: ["CMD", "traefik", "healthcheck", "--ping"]
      timeout: 1s
      interval: 10s
      retries: 3
      start_period: 10s
    image: ${SERVICES_TRAEFIK_IMAGE:-traefik}:${SERVICES_TRAEFIK_IMAGE_VERSION:-latest}
    labels:
      traefik.enable: "true"
      traefik.http.routers.traefik-dashboad.entrypoints: websecure
      traefik.http.routers.traefik-dashboad.middlewares: traefik-dashboard-auth@file
      traefik.http.routers.traefik-dashboad.rule: ${SERVICES_TRAEFIK_LABELS_TRAEFIK_HOST:-HOST(`traefik.yourdomain.com`)}
      traefik.http.routers.traefik-dashboad.service: api@internal
      traefik.http.routers.traefik-dashboad.tls: "true"
      traefik.http.routers.traefik-dashboad.tls.certresolver: tls_resolver
      traefik.http.services.traefik-dashboad.loadbalancer.sticky.cookie.httpOnly: "true"
      traefik.http.services.traefik-dashboad.loadbalancer.sticky.cookie.secure: "true"
      traefik.http.routers.pingweb.rule: PathPrefix(`/ping`)
      traefik.http.routers.pingweb.service: ping@internal
      traefik.http.routers.pingweb.entrypoints: websecure
    networks:
      crowdsec:
        ipv4_address: ${SERVICES_TRAEFIK_NETWORKS_CROWDSEC_IPV4:-172.31.127.253}
        ipv6_address: ${SERVICES_TRAEFIK_NETWORKS_CROWDSEC_IPV6:-fd00:1:be:a:7001:0:3e:6ffe}
      proxy:
        ipv4_address: ${SERVICES_TRAEFIK_NETWORKS_PROXY_IPV4:-172.31.191.254}
        ipv6_address: ${SERVICES_TRAEFIK_NETWORKS_PROXY_IPV6:-fd00:1:be:a:7001:0:3e:7fff}
      socket_proxy:
        ipv4_address: ${SERVICES_TRAEFIK_NETWORKS_SOCKET_PROXY_IPV4:-172.31.255.253}
        ipv6_address: ${SERVICES_TRAEFIK_NETWORKS_SOCKET_PROXY_IPV6:-fd00:1:be:a:7001:0:3e:8ffe}
    ports:
      - mode: host
        target: 80
        published: "80"
        protocol: tcp
      - mode: host
        target: 443
        published: "443"
        protocol: tcp
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ${ABSOLUTE_PATH}/data/traefik/traefik.yml:/etc/traefik/traefik.yml
      - ${ABSOLUTE_PATH}/data/traefik/.htpasswd:/etc/traefik/.htpasswd
      - ${ABSOLUTE_PATH}/data/traefik/certs/acme_letsencrypt.json:/etc/traefik//acme_letsencrypt.json
      - ${ABSOLUTE_PATH}/data/traefik/certs/tls_letsencrypt.json:/etc/traefik/tls_letsencrypt.json
      - ${ABSOLUTE_PATH}/data/traefik/dynamic_conf:/etc/traefik/dynamic_conf:ro
      - /var/log/traefik/:/var/log/traefik/
EOF

#
# Fill Crowdsec compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/networks.yml
networks:
  crowdsec:
    name: ${NETWORKS_CROWDSEC_NAME:-crowdsec}
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
      - subnet: ${NETWORKS_CROWDSEC_SUBNET_IPV4:-172.31.64.0/18}
      - subnet: ${NETWORKS_CROWDSEC_SUBNET_IPV6:-fd00:1:be:a:7001:0:3e:6000/116}
    attachable: true
  proxy:
    name: ${NETWORKS_PROXY_NAME:-proxy}
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
      - subnet: ${NETWORKS_PROXY_SUBNET_IPV4:-172.31.128.0/18}
      - subnet: ${NETWORKS_PROXY_SUBNET_IPV6:-fd00:1:be:a:7001:0:3e:7000/116}
    attachable: true
  socket_proxy:
    name: ${NETWORKS_SOCKET_PROXY_NAME:-socket_proxy}
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
      - subnet: ${NETWORKS_SOCKET_PROXY_SUBNET_IPV4:-172.31.192.0/18}
      - subnet: ${NETWORKS_SOCKET_PROXY_SUBNET_IPV6:-fd00:1:be:a:7001:0:3e:8000/116}
    attachable: true
    internal: true
EOF

#
# Fill Service Blog Bine compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/service-blog-bine.yml
services:
  bine:
    container_name: ${SERVICES_BINE_CONTAINER_NAME:-service-blog-bine}
    image: nginx:latest
    labels:
      traefik.docker.network: proxy
      traefik.enable: true
      traefik.http.routers.bine-secure.entrypoints: websecure
      traefik.http.routers.bine-secure.rule: Host(`bine.rastef.de`)
      traefik.http.routers.bine-secure.service: service-blog-bine
      traefik.http.routers.bine-secure.tls: true
      traefik.http.routers.bine-secure.tls.certresolver: tls_resolver
      traefik.http.routers.bine.entrypoints: web
      traefik.http.routers.bine.rule: Host(`bine.rastef.de`)
      traefik.http.services.bine.loadbalancer.server.port: "80"
    networks:
      default: null
      proxy: null
    restart: unless-stopped
    volumes:
      - ${ABSOLUTE_PATH}/data/service-blog/bine:/usr/share/nginx/html:ro
EOF

#
# Fill Service Blog Ralf compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/service-blog-ralf.yml
services:
  blog:
    container_name: ${SERVICES_BLOG_CONTAINER_NAME:-service-blog-ralf}
    image: nginx:latest
    labels:
      traefik.docker.network: proxy
      traefik.enable: true
      traefik.http.routers.blog-secure.entrypoints: websecure
      traefik.http.routers.blog-secure.rule: Host(`rastef.de`) || Host(`www.rastef.de`)
      traefik.http.routers.blog-secure.service: service-blog-ralf
      traefik.http.routers.blog-secure.tls: true
      traefik.http.routers.blog-secure.tls.certresolver: tls_resolver
      traefik.http.routers.blog.entrypoints: web
      traefik.http.routers.blog.rule: Host(`rastef.de`) || Host(`www.rastef.de`)
      traefik.http.services.blog.loadbalancer.server.port: "80"
    networks:
      default: null
      proxy: null
    restart: unless-stopped
    volumes:
      - ${ABSOLUTE_PATH}/data/service-blog/ralf:/usr/share/nginx/html:ro
EOF

#
# Fill Service Bookstack compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/service-bookstack.yml
services:
  books:
    container_name: bookstack
    image: linuxserver/bookstack:latest
    environment:
      - TZ=Europe/Berlin
      - APP_URL=https://buecher.rastef.de
      - APP_KEY={{get it from Bookstack with: docker run -it --rm --entrypoint /bin/bash lscr.io/linuxserver/bookstack:latest appkey}}
      - DB_HOST=bookstackdb
      - DB_DATABASE=bookstack
      - DB_USERNAME=bookstack
      - DB_PASSWORD={{--Das Bookstack DB Password--}}
    labels:
      traefik.docker.network: proxy
      traefik.enable: true
      traefik.http.routers.books-secure.entrypoints: websecure
      traefik.http.routers.books-secure.rule: Host(`buecher.rastef.de`)
      traefik.http.routers.books-secure.service: bookstack
      traefik.http.routers.books-secure.tls: true
      traefik.http.routers.books-secure.tls.certresolver: tls_resolver
      traefik.http.routers.books.entrypoints: web
      traefik.http.routers.books.rule: Host(`buecher.rastef.de`)
      traefik.http.services.books.loadbalancer.server.port: "80"
    networks:
      default: null
      proxy: null
    restart: unless-stopped
    volumes:
      - ${ABSOLUTE_PATH}/data/bookstack/app:/config
  bookstackdb:
    container_name: booksdb
    image: linuxserver/mariadb:latest
    environment:
      - TZ=Europe/Berlin
      - MYSQL_ROOT_PASSWORD={{--Das MySQL Root Password--}}
      - MYSQL_DATABASE=bookstack
      - MYSQL_USER=bookstack
      - MYSQL_PASSWORD={{--Das Bookstack DB Password--}}
    networks:
      default: null
    restart: unless-stopped
    volumes:
      - ${ABSOLUTE_PATH}/data/bookstack/db:/config
EOF

#
# Fill Service Gitea compose file
#
cat <<'EOF' > /opt/containers/traefik-crowdsec-stack/compose/service-gitea.yml
services:
  gitea:
    container_name: ${SERVICES_GITEA_CONTAINER_NAME:-service-gitea}
    image: docker.gitea.com/gitea:latest-rootless
    labels:
      traefik.docker.network: proxy
      traefik.enable: true
      traefik.http.routers.gitea-secure.entrypoints: websecure
      traefik.http.routers.gitea-secure.rule: Host(`gitea.rastef.de`)
      traefik.http.routers.gitea-secure.service: service-gitea
      traefik.http.routers.gitea-secure.tls: true
      traefik.http.routers.gitea-secure.tls.certresolver: tls_resolver
      traefik.http.routers.gitea.entrypoints: web
      traefik.http.routers.gitea.rule: Host(`gitea.rastef.de`)
      traefik.http.services.gitea.loadbalancer.server.port: "3000"
    networks:
      default: null
      proxy: null
    restart: unless-stopped
    volumes:
      - ${ABSOLUTE_PATH}/data/gitea/repos:/var/lib/gitea
      - ${ABSOLUTE_PATH}/data/gitea/config:/etc/gitea
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
EOF

Mit dem tree Kommando lässt sich die Verzeichnisstruktur verifizieren.  Wenn das tree Kommando fehlt, kann es schnell mit sudo apt install tree installiert werden.

 $ tree -L 4 -a /opt/containers/traefik-crowdsec-stack/

/opt/containers/traefik-crowdsec-stack/
├── compose
│   ├── crowdsec.yml
│   ├── networks.yml
│   ├── service-blog-bine.yml
│   ├── service-blog-ralf.yml
│   ├── service-bookstack.yml
│   ├── service-gitea.yml
│   ├── socket-proxy.yml
│   ├── traefik-crowdsec-bouncer.yml
│   └── traefik.yml
├── data
│   ├── crowdsec
│   │   ├── config
│   │   ├── data
│   │   └── .env
│   ├── service-blog
│   │   ├── bine
│   │   └── ralf
│   ├── service-bookstack
│   │   ├── app
│   │   └── db
│   ├── service-gitea
│   ├── socket-proxy
│   │   └── .env
│   ├── traefik
│   │   ├── certs
│   │   │   ├── acme_letsencrypt.json
│   │   │   └── tls_letsencrypt.json
│   │   ├── dynamic_conf
│   │   │   ├── http.middlewares.default-security-headers.yml
│   │   │   ├── http.middlewares.default.yml
│   │   │   ├── http.middlewares.gzip.yml
│   │   │   ├── http.middlewares.traefik-bouncer.yml
│   │   │   ├── http.middlewares.traefik-dashboard-auth.yml
│   │   │   └── tls.yml
│   │   ├── .env
│   │   ├── .htpasswd
│   │   └── traefik.yml
│   └── traefik-crowdsec-bouncer
│       └── .env
├── docker-compose.yml
└── .env

18 directories, 25 files

Bitte beachten, dass in diversen Dateien noch manuelle Korrekturen vorgenommen werden müssen. Daher bitte noch kein docker compose ausführen. Lesen sie dazu auf der nächsten Seite weiter.