CrowdSec Discord Notifications: Real-Time Security Alerts with Context

:loudspeaker: CrowdSec Discord Notifications: Real-Time Security Alerts with Context

Integrating CrowdSec with Discord lets you receive rich, real-time security alerts directly in your Discord server whenever CrowdSec bans a malicious IP. Instead of just a plain text notification, you’ll get detailed context including geolocation, map previews, attack scenarios, and targeted URIs — making it easier to debug false positives and fine-tune whitelists.


:magnifying_glass_tilted_left: What You Get

  • :police_car_light: Immediate alerts when CrowdSec bans an IP
  • :globe_showing_europe_africa: Geoapify static map image of the attacker’s approximate location
  • :compass: Geolocation details (country, city, maliciousness score if CTI enabled)
  • :link: Clickable IP links to WHOIS for quick investigation
  • :bullseye: Target URIs and metadata to understand attack context
  • :robot: Fully automated Discord delivery via webhooks

Imagine getting a Discord embed that shows:

  • “Ban issued for IP 123.45.67.89 (Maliciousness 85%)”
  • A static map snapshot of the attacker’s origin
  • Target URLs that were hit in your infrastructure

:gear: Requirements

  1. Geoapify API Key

    • Needed for free static map rendering.
    • Get one here: Geoapify
    • Pass it into CrowdSec as GEOAPIFY_API_KEY.
  2. Optional: CrowdSec CTI API Key

    • Provides maliciousness score and detailed geolocation.

    • Free for up to 30 queries/day via CrowdSec Console.

    • Configure in config.yaml:

      api:
        cti:
          key: ${CTI_API_KEY}
          cache_timeout: 60m
          cache_size: 50
          enabled: true
          log_level: debug
      
  3. Discord Webhook

    • Create a webhook in your Discord channel.

    • You’ll get a URL in the format:

      https://discord.com/api/webhooks/${DISCORD_WEBHOOK_ID}/${DISCORD_WEBHOOK_TOKEN}
      
    • Add both ID and token as environment variables.

  4. Notification Config

    • Map a custom discord.yaml to CrowdSec’s notifications directory.
  5. Environment Variables (.env file):

    GEOAPIFY_API_KEY=your-geoapify-key
    DISCORD_WEBHOOK_ID=your-webhook-id
    DISCORD_WEBHOOK_TOKEN=your-webhook-token
    CTI_API_KEY=optional-crowdsec-cti-key
    

:spouting_whale: Docker Compose Example

Here’s a minimal example with Discord + Geoapify integration:

services:
  crowdsec:
    image: crowdsecurity/crowdsec:v1.6.5
    container_name: crowdsec
    environment:
      GEOAPIFY_API_KEY: ${GEOAPIFY_API_KEY}
      DISCORD_WEBHOOK_ID: ${DISCORD_WEBHOOK_ID}
      DISCORD_WEBHOOK_TOKEN: ${DISCORD_WEBHOOK_TOKEN}
      CTI_API_KEY: ${CTI_API_KEY}
    volumes:
      - ./config/acquis.yaml:/etc/crowdsec/acquis.yaml
      - ./config/profiles.yaml:/etc/crowdsec/profiles.yaml
      - ./config/config.yaml:/etc/crowdsec/config.yaml
      - ./notifications/discord.yaml:/etc/crowdsec/notifications/discord.yaml
      - /var/log/traefik:/var/log/traefik/:ro
    restart: unless-stopped

:memo: Discord Notification Template

Your discord.yaml defines the alert format. Example:

type: http
name: discord
format: |
  {
    "embeds": [
      {
        "title": "🚨 CrowdSec Alert",
        "description": "IP {{.Value}} banned for {{.Duration}}",
        "url": "https://app.crowdsec.net/cti/{{.Value}}",
        "image": {
          "url": "https://maps.geoapify.com/v1/staticmap?...&apiKey={{env "GEOAPIFY_API_KEY"}}"
        },
        "fields": [
          { "name": "Scenario", "value": "{{.Scenario}}" },
          { "name": "Country", "value": "{{.Source.Cn}}" },
          { "name": "Target URIs", "value": "{{range (GetMeta . "target_fqdn")}}`{{.}}`\n{{end}}" }
        ]
      }
    ]
  }
url: https://discord.com/api/webhooks/${DISCORD_WEBHOOK_ID}/${DISCORD_WEBHOOK_TOKEN}
method: POST
headers:
  Content-Type: application/json

:wrench: Enable Notifications in Profiles

Add discord to your profiles.yaml:

name: default_ip_remediation
filters:
 - Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
 - type: ban
   duration: 168h
notifications:
 - discord
on_success: break

:test_tube: Testing

Run a manual ban to confirm the notification fires:

docker exec crowdsec cscli decisions add --ip 192.168.1.10 -d 10m

You should see a new embed in your Discord channel with all the configured details.


:white_check_mark: Summary

By integrating CrowdSec with Discord, you now have:

  • Real-time, rich notifications on malicious IP bans
  • Visual map embeds for attacker location
  • Detailed metadata for debugging false positives
  • Optional CTI insights for deeper threat context

This makes it much easier to monitor, react to, and tune your CrowdSec setup without constantly digging into logs.

Thanks for posting, I am getting an error when testing the ban decision:

time=“2025-11-11T19:55:13Z” level=info msg=“(localhost/cscli) manual ‘ban’ from ‘localhost’ by ip 192.168.100.10 : 10m ban on Ip 192.168.100.10”
time=“2025-11-11T19:55:13Z” level=info msg=“127.0.0.1 - [Tue, 11 Nov 2025 19:55:13 UTC] "POST /v1/alerts HTTP/1.1 201 8.479269ms “crowdsec/v1.7.3-c8aad699-docker” "”
time=“2025-11-11T19:55:13Z” level=error msg=“template: :5:27: executing “” at <.Value>: can’t evaluate field Value in type *models.Alert” plugin:=discord

I’ve double-checked the alert template, and it seems to be correct. I’ve also tried a variation I found online and I have the same issue.

CrowdSec Discord Notifications

This module implements a Discord notification mechanism for CrowdSec ban decisions. Upon detection and remediation, it dispatches an alert payload to a specified Discord channel. The notification shows critical metadata on the offending IP address, including geolocational coordinates, and embeds a static map visualization of the inferred origin location.

Additionally, the alert gives all implicated target URIs, facilitating analysis for false positive mitigation and targeted whitelist or parser refinements.

Prerequisites

  1. Geoapify API Integration: Obtain an API key from Geoapify, selected for its free-tier static maps endpoint. Inject this key into the CrowdSec environment via the GEOAPIFY_API_KEY variable (see sample compose.yaml below).

  2. Optional: CrowdSec CTI Smoke Database Access: For enhanced telemetry—such as Maliciousness Scores and granular geolocational enrichment—provision access to the CrowdSec CTI Smoke dataset (free tier: 30 queries/day). Generate a CTI API key via the CrowdSec Console. Configure ingestion in config.yaml (populate CTI_API_KEY environment variable or embed directly):

    api:
      cti:
        key: ${CTI_API_KEY}
        cache_timeout: 60m
        cache_size: 50
        enabled: true
        log_level: debug
    
  3. Discord Webhook Configuration: Establish a Discord webhook and extract its ID and token. The endpoint follows the format https://discord.com/api/webhooks/${DISCORD_WEBHOOK_ID}/${DISCORD_WEBHOOK_TOKEN}. Propagate these via environment variables (reference sample compose.yaml):

    environment:
      GEOAPIFY_API_KEY: ${GEOAPIFY_API_KEY}
      DISCORD_WEBHOOK_ID: ${DISCORD_WEBHOOK_ID}
      DISCORD_WEBHOOK_TOKEN: ${DISCORD_WEBHOOK_TOKEN}
      # Optional: CTI API integration
      CTI_API_KEY: ${CTI_API_KEY}
    
  4. Notification Plugin Mounting: Bind the discord.yaml configuration to /etc/crowdsec/notifications/discord.yaml within the CrowdSec container (consult compose.yaml):

    volumes:
      - ./notifications/discord.yaml:/etc/crowdsec/notifications/discord.yaml
    
  5. Environment Variable Persistence: Define a .env file to externalize sensitive credentials:

    GEOAPIFY_API_KEY=your-geoapify-api-key
    DISCORD_WEBHOOK_ID=your-discord-webhook-id
    DISCORD_WEBHOOK_TOKEN=your-discord-webhook-token
    # Optional: CTI API integration
    CTI_API_KEY=your-cti-api-key-from-crowdsec-console
    
  6. Profile Remediation Update: Amend the profiles.yaml to activate the discord notifier. Exemplar configuration:

    name: default_ip_remediation
    filters:
      - Alert.Remediation == true && Alert.GetScope() == "Ip"
    decisions:
      - type: ban
        duration: 168h
    notifications:
      # - slack_default  # Prerequisite: Configure webhook in /etc/crowdsec/notifications/slack.yaml
      # - splunk_default # Prerequisite: Configure URL and token in /etc/crowdsec/notifications/splunk.yaml
      # - http_default   # Prerequisite: Define HTTP parameters in /etc/crowdsec/notifications/http.yaml
      # - email_default  # Prerequisite: Specify email parameters in /etc/crowdsec/notifications/email.yaml
      - discord
    on_success: break
    
  7. Validation Procedure: Simulate a ban decision to verify notification emission: docker exec crowdsec cscli decisions add --ip 192.168.1.10 -d 10m. Monitor the Discord channel for the resultant payload.

Change the volumes according to pangolin deployment, don’t just blindly copy paste.

services:
  crowdsec:
    image: crowdsecurity/crowdsec:v1.6.5@sha256:fdb487e130095709c1b1c5ba2bd2b461e6715676076e5f774230dcf47d366f76
    container_name: crowdsec
    environment:
      GID: ${GID-1000}
      COLLECTIONS: crowdsecurity/linux crowdsecurity/traefik
      # Get an API Key from Geoapify. Only service I could get a free tier for static maps
      GEOAPIFY_API_KEY: ${GEOAPIFY_API_KEY}
      # Details of Discord webhook. Check your discord webhook url, it will be of the form
      # https://discord.com/api/webhooks/${DISCORD_WEBHOOK_ID}/${DISCORD_WEBHOOK_TOKEN}
      DISCORD_WEBHOOK_ID: ${DISCORD_WEBHOOK_ID}
      DISCORD_WEBHOOK_TOKEN: ${DISCORD_WEBHOOK_TOKEN}
      TZ: ${TZ}
      # Get an API Key for the Crowdsec CTI API if you want Malicousness score and City details
      CTI_API_KEY: ${CTI_API_KEY}
    volumes:
      - ./config/acquis.yaml:/etc/crowdsec/acquis.yaml
      - ./config/profiles.yaml:/etc/crowdsec/profiles.yaml
      - ./config/config.yaml:/etc/crowdsec/config.yaml
      # Make sure to map discord.yaml to /etc/crowdsec/notifications/discord.yaml
      - ./notifications/discord.yaml:/etc/crowdsec/notifications/discord.yaml
      - crowdsec-db:/var/lib/crowdsec/data/
      - crowdsec-config:/etc/crowdsec/
      - /var/log/traefik:/var/log/traefik/:ro
    networks:
      - crowdsec
    security_opt:
      - no-new-privileges:true
    restart: unless-stopped
networks:
  crowdsec:
    external: true
volumes:
  crowdsec-db: null
  crowdsec-config: null
#
# /etc/crowdsec/notifications/discord.yaml
#

type: http
name: discord
log_level: info
format: |
  {
    "embeds": [
      {
        {{range . -}}
        {{$alert := . -}}
        {{range .Decisions -}}
        {{- $cti := .Value | CrowdsecCTI  -}}
        "timestamp": "{{$alert.StartAt}}",
        "title": "Crowdsec Alert",
        "color": 16711680,
        "description": "Potential threat detected. View details in [Crowdsec Console](<https://app.crowdsec.net/cti/{{.Value}}>)",
        "url": "https://app.crowdsec.net/cti/{{.Value}}",
        {{if $alert.Source.Cn -}}
        "image": {
          "url": "https://maps.geoapify.com/v1/staticmap?style=osm-bright-grey&width=600&height=400&center=lonlat:{{$alert.Source.Longitude}},{{$alert.Source.Latitude}}&zoom=8.1848&marker=lonlat:{{$alert.Source.Longitude}},{{$alert.Source.Latitude}};type:awesome;color:%23655e90;size:large;icon:industry|lonlat:{{$alert.Source.Longitude}},{{$alert.Source.Latitude}};type:material;color:%23ff3421;icontype:awesome&scaleFactor=2&apiKey={{env "GEOAPIFY_API_KEY"}}"
        },
        {{end}}
        "fields": [
              {
                "name": "Scenario",
                "value": "`{{ .Scenario }}`",
                "inline": "true"
              },
              {
                "name": "IP",
                "value": "[{{.Value}}](<https://www.whois.com/whois/{{.Value}}>)",
                "inline": "true"
              },
              {
                "name": "Ban Duration",
                "value": "{{.Duration}}",
                "inline": "true"
              },
              {{if $alert.Source.Cn -}}
              { 
                "name": "Country",
                "value": "{{$alert.Source.Cn}} :flag_{{ $alert.Source.Cn | lower }}:",
                "inline": "true"
              }
              {{if $cti.Location.City -}}
              ,
              { 
                "name": "City",
                "value": "{{$cti.Location.City}}",
                "inline": "true"
              },
              { 
                "name": "Maliciousness",
                "value": "{{mulf $cti.GetMaliciousnessScore 100 | floor}} %",
                "inline": "true"
              }
              {{end}}
              {{end}}
              {{if not $alert.Source.Cn -}}
              { 
                "name": "Location",
                "value": "Unknown :pirate_flag:"
              }
              {{end}}
              {{end -}}
              {{end -}}
              {{range . -}}
              {{$alert := . -}}
              {{range .Meta -}}
                ,{
                "name": "{{.Key}}",
                "value": "{{ (splitList "," (.Value | replace "\"" "`" | replace "[" "" |replace "]" "")) | join "\\n"}}"
              } 
              {{end -}}
              {{end -}}
        ]
      }
    ]
  }
url: https://discord.com/api/webhooks/${DISCORD_WEBHOOK_ID}/${DISCORD_WEBHOOK_TOKEN}
method: POST
headers:
  Content-Type: application/json
1 Like

Is this related to my question above?

yes. this is my current setup for discord alerts

1 Like

Ah, okay. It must be something with my setup - the test ban initiates the discord, but fails with the error I posted. I can’t find anyone online with the same error. Not sure what is going on.

Edit: Never mind, it was an issue with the template! Thank you!

1 Like

my template works, it in production env right now