Skip to content

About

Adds Crowdsec as non-invasive- override-feature to mailcow.

Resources

Stars

8 stars

Watchers

0 watching

Forks

Repository files navigation

Mailcow CrowdSec Integration

Adds CrowdSec threat detection and IP banning to a Mailcow Dockerized installation via docker-compose.override.yml — no changes to Mailcow's own files, fully upgrade-safe.

What this does

  • Detects brute-force attacks on SMTP (Postfix), IMAP/POP3 (Dovecot), and the web UI (nginx) by reading their Docker logs
  • Bans attacking IPs at the iptables level (INPUT, FORWARD, DOCKER-USER chains) using ipsets — blocks before traffic reaches any service
  • Shares threat intelligence with the CrowdSec community network, pulling a live blocklist of known bad actors
  • Geo-aware thresholds — your home country gets a lenient threshold (ban after 3 failed logins); the rest of the world is banned immediately on the first attempt
  • Escalating bans — persistent returners across SMTP, IMAP/POP3, and HTTP receive a 7-day ban after repeated offences
  • Zero Mailcow modifications — everything is in docker-compose.override.yml, which Docker Compose merges automatically

Architecture

Mailcow containers (nginx, postfix, dovecot)
        │ logs via Docker socket
        ▼
┌─────────────────────────────┐
│  crowdsec container         │  ← LAPI + agent, port 127.0.0.1:8080
│  reads logs, detects attacks│
│  maintains ban decisions    │
└────────────┬────────────────┘
             │ decisions via API
             ▼
┌─────────────────────────────┐
│  cs-firewall-bouncer        │  ← network_mode: host
│  enforces bans in iptables  │
│  INPUT / FORWARD / DOCKER-USER chains
└─────────────────────────────┘

Prerequisites

  • Mailcow Dockerized installed and running
  • Docker Compose v2
  • Root or sudo access on the host

Installation

1. Copy files into your Mailcow directory

cp docker-compose.override.yml /opt/mailcow-dockerized/
cp -r crowdsec/ /opt/mailcow-dockerized/

If you already have a docker-compose.override.yml (e.g. for the Prometheus exporter), merge the services: and volumes: blocks manually instead of overwriting.

2. Adjust container names in acquis.yaml

Mailcow container names follow the pattern <project>-<service>-<index>. The default project name is mailcowdockerized, giving names like mailcowdockerized-nginx-mailcow-1.

Verify your actual container names:

docker ps --format '{{.Names}}' | grep mailcow

If your names differ, edit /opt/mailcow-dockerized/crowdsec/acquis.yaml accordingly.

3. Adjust whitelists

Edit /opt/mailcow-dockerized/crowdsec/parsers/s02-enrich/whitelists.yaml and add any IPs or CIDRs that should never be banned — your home IP, monitoring server, VPN range, etc.

4. Configure geo-aware thresholds

The included SMTP brute-force scenarios apply different ban thresholds by country:

Group Threshold Scenario file
Lenient (home country) Ban after 3 failed logins within 1 hour crowdsec/scenarios/postfix-sasl-brute-de.yaml
Strict (rest of world) Ban on first failed login crowdsec/scenarios/postfix-sasl-brute-world.yaml

The files ship configured for Germany (DE). To use a different country, edit both scenario files and replace DE with your country's ISO 3166-1 alpha-2 code (e.g. FR for France, NL for Netherlands, US for United States):

# Example: switch from Germany to Austria
sed -i "s/IsoCode == 'DE'/IsoCode == 'AT'/g" \
    /opt/mailcow-dockerized/crowdsec/scenarios/postfix-sasl-brute-de.yaml

sed -i "s/IsoCode != 'DE'/IsoCode != 'AT'/g" \
    /opt/mailcow-dockerized/crowdsec/scenarios/postfix-sasl-brute-world.yaml

To grant the lenient threshold to multiple countries (e.g. DE, AT, CH), edit the filter lines manually:

# postfix-sasl-brute-de.yaml
filter: "evt.Meta.log_type_enh == 'spam-attempt' && evt.Enriched.IsoCode in ['DE', 'AT', 'CH']"

# postfix-sasl-brute-world.yaml
filter: "evt.Meta.log_type_enh == 'spam-attempt' && !(evt.Enriched.IsoCode in ['DE', 'AT', 'CH'])"

To disable geo-awareness entirely and apply a single threshold for all IPs, delete postfix-sasl-brute-de.yaml, keep only postfix-sasl-brute-world.yaml, and set whatever capacity you prefer. Remove the -de bind mount from docker-compose.override.yml.

After any scenario change, restart CrowdSec:

cd /opt/mailcow-dockerized && docker compose restart crowdsec

5. Start CrowdSec

cd /opt/mailcow-dockerized
docker compose pull crowdsec
docker compose up -d crowdsec

Wait ~10 seconds for startup, then verify log acquisition:

docker exec mailcowdockerized-crowdsec-1 cscli metrics show acquisition

All three sources (nginx, postfix, dovecot) should show non-zero Lines read.

6. Generate the bouncer API key

BOUNCER_KEY=$(docker exec mailcowdockerized-crowdsec-1 cscli bouncers add firewall-bouncer -o raw)
echo "Key: $BOUNCER_KEY"

7. Create the bouncer config

cp /opt/mailcow-dockerized/crowdsec/cs-firewall-bouncer.yaml.example \
   /opt/mailcow-dockerized/crowdsec/cs-firewall-bouncer.yaml

# Replace the placeholder with the generated key:
sed -i "s/REPLACE_WITH_YOUR_BOUNCER_API_KEY/${BOUNCER_KEY}/" \
   /opt/mailcow-dockerized/crowdsec/cs-firewall-bouncer.yaml

8. Build and start the firewall bouncer

The bouncer is built from crowdsec/Dockerfile.bouncer. It automatically fetches the latest release from GitHub at build time — no version to maintain manually.

cd /opt/mailcow-dockerized
docker compose build --no-cache cs-firewall-bouncer
docker compose up -d cs-firewall-bouncer

9. Verify

# Bouncer is registered and valid
docker exec mailcowdockerized-crowdsec-1 cscli bouncers list

# iptables chains exist
iptables -L INPUT | grep CROWDSEC
iptables -L FORWARD | grep CROWDSEC

# Active bans (may be empty immediately, populates within minutes)
docker exec mailcowdockerized-crowdsec-1 cscli decisions list

Optional: Enroll in CrowdSec Console

The CrowdSec Console gives you a web UI to view alerts, bans, and threat intelligence across all your enrolled machines.

docker exec mailcowdockerized-crowdsec-1 cscli console enroll <your-enrollment-id>

Accept the enrollment at https://app.crowdsec.net, then restart CrowdSec:

cd /opt/mailcow-dockerized && docker compose restart crowdsec

Usage

Check active bans

docker exec mailcowdockerized-crowdsec-1 cscli decisions list

View alerts (what triggered a ban)

docker exec mailcowdockerized-crowdsec-1 cscli alerts list

Unban an IP (e.g. if you locked yourself out)

docker exec mailcowdockerized-crowdsec-1 cscli decisions delete --ip <ip>

Ban an IP manually

docker exec mailcowdockerized-crowdsec-1 cscli decisions add --ip <ip> --reason "manual ban" --duration 24h

Update CrowdSec hub (parsers and scenarios)

docker exec mailcowdockerized-crowdsec-1 cscli hub update
docker exec mailcowdockerized-crowdsec-1 cscli hub upgrade
docker compose restart crowdsec

Add more collections (e.g. SSH brute force)

Edit docker-compose.override.yml, update the COLLECTIONS environment variable:

COLLECTIONS: "crowdsecurity/nginx crowdsecurity/postfix crowdsecurity/dovecot crowdsecurity/sshd"

Then recreate the container:

cd /opt/mailcow-dockerized && docker compose up -d crowdsec

Upgrading the bouncer

The Dockerfile resolves the latest release automatically via the GitHub API. To upgrade:

cd /opt/mailcow-dockerized
docker compose build --no-cache cs-firewall-bouncer
docker compose up -d cs-firewall-bouncer

--no-cache is required to bypass Docker's layer cache and force a fresh GitHub API lookup. To pin a specific version instead:

docker compose build --no-cache --build-arg VERSION=v0.0.34 cs-firewall-bouncer
docker compose up -d cs-firewall-bouncer

Keeping the bouncer current after Mailcow updates

Mailcow runs post_update_hook.sh from the install root at the end of every ./update.sh. Copy the example so the firewall bouncer is rebuilt (and re-fetches its latest release) automatically after each Mailcow update:

cp post_update_hook.sh.example /opt/mailcow-dockerized/post_update_hook.sh
chmod +x /opt/mailcow-dockerized/post_update_hook.sh

The hook also pulls and restarts FederLoom if it is installed (see below); it is a no-op otherwise.

Optional: FederLoom federated reputation sharing

FederLoom is a decentralized, federated IP-reputation sidecar. Where CrowdSec shares intel through its central community network, FederLoom shares trust-weighted reputation peer-to-peer between self-hosted servers. It runs alongside this CrowdSec integration — CrowdSec for local detection and enforcement, FederLoom for federated intel — and consumes the same local CrowdSec LAPI.

A helper script installs and auto-configures it:

cd /opt/mailcow-dockerized   # or wherever this repo's files were copied
./federloom/setup-federloom.sh /opt/mailcow-dockerized

The script auto-detects your public IP, Tailscale IP, Docker networks, and Mailcow container names and asks you to confirm before applying. It registers a CrowdSec bouncer named federloom, writes federloom/config.local.yaml (which is gitignored — it holds the API key), fetches rules.yaml from upstream, merges a federloom service into your existing docker-compose.override.yml (after a timestamped backup), and starts the container. Re-running it is safe.

After it starts, open inbound tcp/7700 in your firewall for full peer-to-peer federation.

Joining the maintainer's federation

The script offers (opt-in) to join the federloom.jru.me honeypot federation using the bundled trust invite. Verify this fingerprint out-of-band first:

79bb d13a 114b 88fe

The setup script saves the invite to federloom/federation.invite. To join later by hand:

cd /opt/mailcow-dockerized
docker compose cp federloom/federation.invite federloom:/tmp/federation.invite
docker compose exec federloom federloomctl federation join /tmp/federation.invite \
    --config /etc/federloom/config.yaml

Compatibility with Mailcow updates

Mailcow's ./update.sh uses docker compose from the installation directory, which automatically merges docker-compose.override.yml. CrowdSec services are unaffected by Mailcow updates.

File overview

docker-compose.override.yml                        ← drop into /opt/mailcow-dockerized/
crowdsec/
  acquis.yaml                                      ← log sources (adjust container names)
  Dockerfile.bouncer                               ← builds the firewall bouncer image
  cs-firewall-bouncer.yaml.example                 ← copy to .yaml and fill in API key
  parsers/
    s01-parse/
      postfix-auth-failures.yaml                   ← catches disconnect auth=0/N lines
    s02-enrich/
      whitelists.yaml                              ← trusted IPs/CIDRs (adjust to your setup)
  scenarios/
    postfix-sasl-brute-world.yaml                  ← strict: ban on 1st failed login (all non-home-country IPs)
    postfix-sasl-brute-de.yaml                     ← lenient: ban after 3 failed logins (home country, default: DE)
    postfix-sasl-recidive.yaml                     ← escalation: 7-day ban after 3 SMTP returns within 24h
    dovecot-recidive.yaml                          ← escalation: 7-day ban after 2 IMAP/POP3 returns within 24h
    http-recidive.yaml                             ← escalation: 7-day ban after 3 HTTP scan returns within 24h

Escalating ban logic

Persistent attackers that return after their initial ban receive progressively longer bans via dedicated recidive scenarios. Each scenario watches the same events as its base counterpart but uses a 24-hour leaky bucket — events drain slowly, so only IPs that return within 24h of their previous attempt accumulate toward the escalated ban.

Protocol Base scenario Escalation trigger Escalated ban
SMTP AUTH 1st failed connection → 4h (postfix-sasl-brute-world) 3 returns within 24h 7 days
IMAP/POP3 4 auth failures in 6 min → 4h (dovecot-spam) 2 returns within 24h 7 days
HTTP scan 10 unique 404/403/400 paths → 4h (http-probing) 3 return visits within 24h 7 days

IPs that space their attacks more than 24h apart continue receiving short bans from the base scenarios — the recidive bucket drains before they return. Only persistent same-day returners escalate.

Security notes

  • cs-firewall-bouncer.yaml contains the LAPI API key — it is listed in .gitignore and must not be committed
  • The bouncer runs with network_mode: host and CAP_NET_ADMIN / CAP_NET_RAW — required to manipulate iptables on the host
  • The Docker socket (/var/run/docker.sock) is mounted read-only into the CrowdSec container for log reading — standard practice for log collectors

References

About

Adds Crowdsec as non-invasive- override-feature to mailcow.

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages