Skip to main content
  1. Posts/
  2. Traefik/

Installing Traefik in an LXC Container on Proxmox as a systemd Service | Part 3 - CrowdSec

··1587 words·8 mins· loading · loading · ·
Stilicho2011
Author
Stilicho2011
Writing about homelab, self-hosting, automation and open-source solutions
Table of Contents
Traefik - This article is part of a series.
Part : This Article

Introduction
#

In part 2, in the “what’s left” section, I put off installing CrowdSec for a separate pass. This is that pass. CrowdSec is installed in the same LXC as Traefik itself, rather than as a separate Docker container like before. This simplifies connectivity (no Docker DNS names, everything talks over localhost) and lets CrowdSec read access.log directly from disk, without shuttling the file between different environments.


Installation
#

CrowdSec is installed via the official repository-add script, then through regular apt:

curl -s https://install.crowdsec.net | sudo sh
apt update
apt list crowdsec
apt install crowdsec

apt list crowdsec is an optional step, just to confirm the package actually shows up in the list after adding the repository, before installing it.


Collections
#

A collection is a ready-made set of detection scenarios for a particular type of workload. Before installing, I update the hub index to make sure I’m installing current versions rather than whatever was baked into the package at build time:

cscli hub update

I install the package for the HTTP perimeter (Traefik) right away, plus the base scenarios for the OS itself:

cscli collections install crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/base-http-scenarios crowdsecurity/sshd crowdsecurity/linux crowdsecurity/appsec-generic-rules crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-crs
  • crowdsecurity/traefik - access-log parser specifically for the Traefik format;
  • crowdsecurity/http-cve + base-http-scenarios - detection of known CVE exploitation attempts and generic HTTP attacks;
  • crowdsecurity/sshd + linux - since CrowdSec now lives in the same LXC as Traefik, I’m covering SSH access to the container itself at the same time;
  • appsec-generic-rules, appsec-virtual-patching, appsec-crs - for the AppSec component (WAF-like protection at the request level, before it reaches the backend) - more on that separately below.
systemctl reload crowdsec

Automatic hub updates on a schedule
#

Scenarios and parsers in the hub are updated periodically (new CVEs, fixes to existing detections), but manually remembering to run cscli hub update is rare. I put it in a cron file so the index update and upgrade of installed collections happen on their own once a day, at midnight:

nano /etc/cron.d/crowdsec-hub-update
0 0 * * * root /usr/bin/cscli hub update && /usr/bin/cscli hub upgrade

A file in /etc/cron.d/ doesn’t require crontab -e and isn’t tied to a user crontab - it’s picked up automatically by the system cron once saved, without an additional daemon reload. hub upgrade pulls in new versions of already-installed collections/parsers/scenarios (it doesn’t install new ones - only updates what’s already enabled). If the upgrade actually changed something, cscli will itself prompt for systemctl reload crowdsec - you can either add that as a third command on the same cron line, or, as I prefer, leave it under manual control, so you don’t reload the production service’s configuration without reviewing what actually got updated.


Remediation profile
#

The profile determines exactly what to do when CrowdSec decides to block something - not just “ban it,” but by what logic and where to notify.

nano /etc/crowdsec/profiles.yaml:

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

---

name: default_range_remediation
filters:
 - Alert.Remediation == true && Alert.GetScope() == "Range"
decisions:
 - type: ban
   duration: 4h
notifications:
 - http_default
on_success: break

Two profiles - one for banning a specific IP, another for banning an entire range (if the attack is distributed across a subnet). Both ban for 4 hours and send a notification through the http_default plugin, which is configured separately.


Notifications via Gotify
#

Since I already have Gotify in my stack (push notifications to phone/desktop), it makes sense to route CrowdSec alerts there too, rather than setting up an email integration from scratch (other options don’t officially work in Russia as of this writing).

nano /etc/crowdsec/notifications/http.yaml:

type: http
name: http_default
log_level: info
format: |
  {{ range . -}}
  {{ $alert := . -}}
  {
    "extras": {
      "client::display": {
      "contentType": "text/markdown"
    }
  },
  "priority": 3,
  {{range .Decisions -}}
  "title": "{{.Type }} {{ .Value }} for {{.Duration}}",
  "message": "{{.Scenario}}  \n\n[crowdsec cti](https://app.crowdsec.net/cti/{{.Value -}})  \n\n[shodan](https://shodan.io/host/{{.Value -}})"
  {{end -}}
  }
  {{ end -}}
url: https://gotify.domain.ru/message
method: POST
headers:
  X-Gotify-Key: your_application_token
  Content-Type: application/json

The template produces a Gotify-compatible JSON: a title with the decision type and value (IP/range, ban duration), a message body with the scenario that triggered, plus ready-made links to CrowdSec CTI and Shodan for a quick manual check of the address.

X-Gotify-Key is the application token for a specific application in Gotify (created in its own UI), not a user token.


Log sources (acquisition)
#

The CrowdSec version we’re installing uses the /etc/crowdsec/acquis.d/ directory - a separate file per source, instead of one monolithic acquis.yaml. Same logic as the split of dynamic/ for Traefik itself: sources are easier to read and maintain when they’re not lumped together in one pile.

Traefik access logs
#

touch /etc/crowdsec/acquis.d/traefik.yaml
poll_without_inotify: false
filenames:
  - /var/log/traefik/*.log
labels:
  type: traefik

poll_without_inotify: false - use inotify (more efficient than polling the file on a timer) to track new lines in the log.

Through the *.log mask, this source picks up not just access.log but also traefik.log (Traefik’s own operational log - startup, config reload, TLS certificate issuance). The crowdsecurity/traefik-logs parser is built for the access-log format, so lines from traefik.log will honestly show up as “unparsed” in cscli metrics - this is expected, not a config error, and there’s no need to remove traefik.log from the source.

System logs
#

touch /etc/crowdsec/acquis.d/syslog.yaml
filenames:
  - /var/log/auth.log
  - /var/log/syslog
labels:
  type: syslog

This is what we installed the sshd/linux collections for - since CrowdSec is now in the same LXC that receives external traffic, it makes sense to also keep an eye on SSH brute-force attempts and suspicious OS-level activity.

Debian 13 (trixie): rsyslog isn’t installed by default
#

On Debian 13, this source won’t work on its own. Starting with bookworm, rsyslog is no longer installed by default - /var/log/auth.log and /var/log/syslog simply don’t exist or aren’t written to unless the package is explicitly installed. Check with:

ls -la /var/log/auth.log /var/log/syslog

If auth.log is missing and syslog exists but is zero bytes - rsyslog isn’t installed:

apt install rsyslog
systemctl enable --now rsyslog

Next comes a trap specific to trixie: even after installing the package, it does not create the routing rules file /etc/rsyslog.d/50-default.conf, which used to come “out of the box” before. The /etc/rsyslog.d/ directory is empty after installation (except possibly postfix.conf, if Postfix is installed too). Without these rules, rsyslogd listens to the journal but doesn’t know where to write auth/authpriv messages - the files stay empty. Create it manually:

nano /etc/rsyslog.d/50-default.conf

The minimum CrowdSec needs:

auth,authpriv.*                /var/log/auth.log
*.*;auth,authpriv.none         -/var/log/syslog
systemctl restart rsyslog

Verification - any action via sudo (for example sudo -k && sudo whoami, to clear the cache and go through PAM again) should immediately show up in auth.log:

ls -la /var/log/auth.log /var/log/syslog

Both files should exist and grow. After that, systemctl restart crowdsec and cscli metrics - Acquisition Metrics should show entries for file:/var/log/auth.log and file:/var/log/syslog with a non-zero number of lines read.

systemctl reload crowdsec
cscli metrics

cscli metrics shows whether CrowdSec is seeing any lines at all from the connected sources - if the counters are zero, there’s no point starting on AppSec yet; acquisition needs to be sorted out first.


AppSec component
#

AppSec is WAF-like protection at the level of the request itself (attack signatures, virtual patching for known CVEs), operating separately from the classic anti-brute-force log analysis. Traefik talks to it directly through the bouncer plugin, before the request reaches the actual backend.

touch /etc/crowdsec/acquis.d/appsec.yaml
listen_addr: 127.0.0.1:7422
appsec_config: crowdsecurity/appsec-default
name: myAppSecComponent
source: appsec
labels:
    type: appsec
Important to check before starting: the address must be 127.0.0.1, not 127.0.0.0 - the latter is the address of the 127.0.0.0/8 network itself, not a host, and isn’t guaranteed to bind correctly. Traefik’s bouncer plugin below uses localhost:7422, which resolves to 127.0.0.1 - both addresses must match literally, otherwise AppSec will be unreachable, and crowdsecAppsecUnreachableBlock: true in the plugin config will block all traffic through Traefik if the component isn’t responding.
systemctl reload crowdsec
systemctl reload traefik
cscli metrics show acquisition

Registering the bouncer and connecting it to Traefik
#

cscli bouncers add traefik-bouncer

The command outputs the API key once - save it immediately, you won’t be able to view it again (same logic as with the Cloudflare token in part 2), only recreate it.

Middleware in dynamic/middlewares/crowdsec.yaml:

http:
  middlewares:
    crowdsec:
      plugin:
        bouncer:
          enabled: true
          logLevel: INFO
          updateIntervalSeconds: 15
          updateMaxFailure: 0
          defaultDecisionSeconds: 15
          httpTimeoutSeconds: 10
          crowdsecMode: stream
          crowdsecAppsecEnabled: true
          crowdsecAppsecHost: localhost:7422
          crowdsecAppsecFailureBlock: true
          crowdsecAppsecUnreachableBlock: true
          crowdsecLapiKey: your_key_from_cscli_bouncers_add
          crowdsecLapiHost: localhost:8080
          crowdsecLapiScheme: http
          forwardedHeadersTrustedIPs:
            - 10.0.0.0/8
            - 172.16.0.0/12
            - 192.168.0.0/16
          clientTrustedIPs:
            - 10.0.0.0/8
            - 172.16.0.0/12
            - 192.168.0.0/16

localhost instead of a Docker name like crowdsec:8080, which is what the old Docker-based setup would have used - both services are now in the same LXC, so no additional network connectivity is needed at all.

Last step - uncomment the middleware in the static config (it was commented out in part 2):

entryPoints:
  web:
    http:
      middlewares:
        - crowdsec@file   # now can be uncommented
        - rate-limit@file
  websecure:
    http:
      middlewares:
        - crowdsec@file   # and here too
        - rate-limit@file
systemctl restart traefik

Verification
#

cscli decisions list      # currently active bans
cscli metrics             # overall stats on scenarios and bouncers
cscli bouncers list        # confirms that traefik-bouncer connected and is polling LAPI

If cscli bouncers list shows the bouncer but with a stale last-request time - check the connectivity between Traefik and localhost:8080 (the mere fact that both are in the same LXC doesn’t guarantee the plugin is pointed at the right port/scheme).

Links#

Traefik - This article is part of a series.
Part : This Article

Related

Installing Traefik in an LXC Container on Proxmox as a systemd Service | Part 2

··3160 words·15 mins· loading · loading
Continuing the series about Traefik in LXC - from a test binary to a working configuration. Cloudflare token, final static config, wildcard certificate, dashboard protection via Basic Auth, log rotation, dynamic config structure, and migrating a dozen services from Docker labels to the file provider. CrowdSec is planned as a separate follow-up.