CrowdSec on a VPS: Collective fail2ban for SSH, Nginx, and Docker
TL;DR
CrowdSec is installed on a VPS as a log analysis daemon: it detects suspicious activity in SSH, Nginx, and Docker containers, then blocks malicious IP addresses through firewall-bouncer. Unlike classic fail2ban, CrowdSec can use collective IP reputation, but the blocking decision and enforcement remain on your server.
- For a small production server, 2 vCPU, 2–4 GB RAM, and 40–80 GB SSD are sufficient.
- SSH, Nginx, Docker logs, and firewall-bouncer for nftables will be configured.
- Caddy with automatic Let’s Encrypt certificate issuance is used for HTTPS.
- CrowdSec configuration is stored separately from automatically updated files.
- At the end, verification, backups, updates, and troubleshooting of common errors will be configured.
1. TL;DR
CrowdSec is a modern attack detection and blocking system often called collective fail2ban. It reads system and application logs, applies detection scenarios, and passes decisions to firewall-bouncer. As a result, SSH password brute-force attempts, Nginx scanning, and some attacks on Docker services are blocked before an attacker gets many attempts.
Neither CrowdSec nor fail2ban replaces updates, SSH keys, least privilege, or proper Docker port exposure. The main goal of this guide is to achieve layered protection for a single VPS without installing a separate management platform.
- Operating system: Debian 12 or Ubuntu Server 24.04 LTS.
- CrowdSec: 1.6.x branch, the current stable branch as of 2026.
- Reverse proxy: Nginx 1.26+ or Caddy 2.9+; both components are used in the example for different scenarios.
- Firewall: nftables through firewall-bouncer.
- Docker Engine: current stable 27/28 branch, installed from the official Docker repository.
2. Contents
The guide is intended for a clean VPS with a public IPv4 address. The commands are provided for Debian 12 and Ubuntu 24.04; package names and paths are practically identical on these systems.
If Nginx, Docker, or fail2ban are already running on the server, save their configuration before installation. It is especially important to check which ports are published by Docker containers: publishing a port through Docker can bypass ordinary ufw rules, so in production it is better to control access at the Docker, nftables, and reverse proxy levels.
- First, a separate administrator is created and SSH is restricted.
- Then the basic components and CrowdSec are installed.
- After that, scenario collections for Linux, SSH, Nginx, and Docker are connected.
- Firewall-bouncer is enabled, turning CrowdSec decisions into actual blocks.
- Logs, scenarios, the firewall, and HTTPS are tested.
3. What we are configuring and why
How CrowdSec works
CrowdSec consists of several logical components. The Local API stores decisions and receives events from the security engine. The engine reads logs, passes lines to parsers and scenarios, and the scenarios identify behavior such as password brute-forcing, path scanning, a large number of errors, or signs of a known exploit pattern.
When a scenario is triggered, a decision is created, such as blocking an IP for four hours. The Bouncer receives this decision and applies it to incoming traffic. For Linux with nftables, this is usually a separate chain in netfilter, so the web server does not need to implement blocking itself.
| Component | Purpose | What to check |
|---|---|---|
| Security engine | Reads logs and runs scenarios | cscli metrics |
| Parsers | Convert log lines into events | cscli hub list |
| Scenarios | Identify suspicious sequences | cscli scenarios list |
| Firewall-bouncer | Blocks IPs through nftables | cscli bouncers list |
| Central API | Publishes and receives collective reputation | Registration and pull status |
Why this is not simply a fail2ban replacement
Fail2ban usually analyzes only local logs and creates a local blocking rule. This works well against recurring attacks on a specific server. CrowdSec adds a unified scenario engine, convenient collection management, and the ability to use signals from other installations through the Central API.
Collective reputation does not mean that every address is automatically considered dangerous forever. Decisions have a type, source, and expiration period. The administrator can view or delete a decision or add a custom one. For critical systems, it is recommended to enable monitoring first and check for false positives, then expand blocking.
What will be protected
- SSH: password brute-forcing, a large number of failed connections, and scanning attempts.
- Nginx: mass requests to nonexistent paths, suspicious URLs, and some known attack patterns.
- Docker: protection of application and reverse proxy logs rather than the container itself. If an application writes useful events to stdout, they can be collected through the Docker logging driver or a file.
- System firewall: blocking IPs at the kernel level, before requests are processed by the application.
Self-hosted and cloud-managed options
Cloud-managed WAF, CDN, and security gateway solutions are convenient when you need to protect many domains, distribute traffic across regions, or automatically filter volumetric DDoS attacks. You have to pay monthly for this, route DNS through a third-party network, and accept the provider’s restrictions.
Self-hosted CrowdSec on a VPS is suitable for a single server, a small team, a private API, a Git server, an administration panel, and applications where full control over logs is important. However, a VPS cannot stop a large DDoS attack: if the provider’s link or virtual NIC is saturated, the local firewall will no longer help.
4. What VPS configuration is needed for this task
CrowdSec consumes little CPU and memory. The primary load depends not on the agent itself but on the volume of logs, the number of scenarios, Nginx request rates, and the number of containers. Requirements are minimal for SSH and one website; for multiple applications, their own RAM requirements must be taken into account.
| Scenario | CPU | RAM | Disk | Network |
|---|---|---|---|---|
| SSH and a small Nginx setup | 1 vCPU | 1 GB | 20–30 GB SSD | 100 Mbps |
| Docker, 2–5 services, and CrowdSec | 2 vCPU | 4 GB | 60–80 GB NVMe | 1 Gbps |
| Multiple websites and CI tasks | 4 vCPU | 8 GB | 100–160 GB NVMe | 1 Gbps |
A practical option for most installations
For this guide, a reasonable choice is 2 vCPU, 4 GB RAM, 60–80 GB NVMe, one public IPv4 address, and a 1 Gbps port. This capacity allows you to run CrowdSec, Nginx or Caddy, Docker Compose, and several small applications simultaneously. If the containers use PostgreSQL, Elasticsearch, GitLab, or image builds, RAM should be increased separately.
As one option, you can get a VPS with these specifications. The important factors are the resources, disk stability, backup availability, and the ability to set a reverse DNS record—not the name of a particular plan.
When a dedicated server is needed
A dedicated server is not needed because of CrowdSec itself. It becomes worthwhile if the machine runs databases with high I/O, dozens of containers, CI/CD workers, game servers, a large GitLab instance, your own blockchain node, or several virtual machines.
For CrowdSec, at least one vCPU and 512 MB RAM should be allocated, but on a dedicated server it is convenient to reserve 2–4 vCPU and 4–8 GB RAM for the security stack and system services. Do not keep the only copy of your backup archives on the same physical server.
Impact of location
Location primarily affects latency to users and legal requirements for data. For CrowdSec, distance to the Central API is usually not critical: events are analyzed locally. For a web application, choose a region closer to your main audience, while for an administrative VPS, consider the route from your workplace.
Check whether the provider offers a PTR record, IPv6, outbound SMTP filtering, and the ability to open the required inbound ports. Also clarify the policy regarding security scanners: CrowdSec must not be used for active scanning of other people’s networks.
5. Server preparation
5. Server Preparation
Login and Administrator Creation
The name deploy is used below. Replace it with your own. Perform the first login as root only for initial setup, and then use sudo. Before changing SSH, keep the current session open and test the new login in a second terminal.
# Обновляем индексы пакетов
sudo apt update
# Устанавливаем обновления безопасности и исправления
sudo apt full-upgrade -y
# Создаём отдельного пользователя
sudo adduser deploy
# Разрешаем ему выполнять административные команды
sudo usermod -aG sudo deploy
# Создаём каталог для публичных SSH-ключей
sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
From the local computer, add the public key. Do not copy the private key to the server.
# Выполняется на вашем рабочем компьютере
ssh-copy-id deploy@SERVER_IP
# Проверяем вход новым пользователем в отдельном терминале
ssh deploy@SERVER_IP
Basic Packages
# Устанавливаем диагностику, шифрование, редактор и управление репозиториями
sudo apt install -y \
ca-certificates curl gnupg jq git vim htop lsof unzip \
apt-transport-https software-properties-common \
nftables rsyslog logrotate
# Проверяем время и синхронизацию часов
timedatectl status
# Включаем системную синхронизацию времени
sudo timedatectl set-ntp true
SSH hardening
First, make sure that key-based login works. After that, disable password authentication and root login. If you have automation that still uses a password, it will stop working after applying the file.
# Создаём отдельный drop-in для sshd
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf >/dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 20
X11Forwarding no
AllowUsers deploy
EOF
# Проверяем синтаксис до перезапуска
sudo sshd -t
# Применяем конфигурацию без закрытия текущих сессий
sudo systemctl reload ssh
Firewall Before Installing CrowdSec
Open only SSH, HTTP, and HTTPS. If SSH runs on another port, replace the value in the rules. Do not expose database and Docker service ports to the public Internet unless necessary.
# Включаем nftables при загрузке
sudo systemctl enable --now nftables
# Создаём базовый firewall: loopback, established, SSH и web
sudo tee /etc/nftables.conf >/dev/null <<'EOF'
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
iifname "lo" accept
ct state established,related accept
ct state invalid drop
ip protocol icmp accept
ip6 nexthdr ipv6-icmp accept
tcp dport 22 accept
tcp dport { 80, 443 } accept
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
EOF
# Загружаем правила
sudo nft -f /etc/nftables.conf
# Проверяем активный набор правил
sudo nft list ruleset
Is fail2ban Needed?
In this assignment, fail2ban is installed as a basic safeguard, but you should not blindly run two independent banning mechanisms with identical thresholds. Otherwise, the systems may block addresses according to their own rules, making troubleshooting more difficult. After verifying CrowdSec, it is usually best to leave only CrowdSec for SSH and web, and disable fail2ban or use it for a separate application.
# Устанавливаем fail2ban как временный или резервный механизм
sudo apt install -y fail2ban
# Создаём безопасный локальный конфиг SSH jail
sudo tee /etc/fail2ban/jail.d/sshd.local >/dev/null <<'EOF'
[sshd]
enabled = true
backend = systemd
port = ssh
maxretry = 6
findtime = 10m
bantime = 10m
EOF
# Перезапускаем fail2ban и смотрим состояние
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
After putting CrowdSec into production, you can disable this jail and leave fail2ban installed for manual emergency activation:
# Отключаем дублирующий SSH-jail после успешной проверки CrowdSec
sudo sed -i 's/^enabled = true/enabled = false/' /etc/fail2ban/jail.d/sshd.local
sudo systemctl restart fail2ban
6. Installing CrowdSec, Nginx, and Docker
Installing CrowdSec
On Debian and Ubuntu, use the official CrowdSec repository rather than random PPAs. The package version may be newer than the one specified in the article. In 2026, use the stable 1.6.x branch as a reference and verify the package before pinning the version.
# Добавляем официальный signing key CrowdSec
curl -fsSL https://packagecloud.io/crowdsec/crowdsec/gpgkey \
| sudo gpg --dearmor -o /usr/share/keyrings/crowdsec-archive-keyring.gpg
# Добавляем официальный apt-репозиторий
echo "deb [signed-by=/usr/share/keyrings/crowdsec-archive-keyring.gpg] https://packagecloud.io/crowdsec/crowdsec/debian/ bookworm main" \
| sudo tee /etc/apt/sources.list.d/crowdsec.list
# Обновляем индексы и устанавливаем CrowdSec 1.6.x
sudo apt update
sudo apt install -y crowdsec
# Включаем сервис и проверяем установленную версию
sudo systemctl enable --now crowdsec
sudo cscli version
On Ubuntu 24.04, replace the repository codename with one supported by the official repository if the current entry is unsuitable. The apt policy crowdsec check will show where the package came from.
# Показываем источник и доступную версию пакета
apt policy crowdsec
# Проверяем состояние systemd-сервиса
sudo systemctl status crowdsec --no-pager
Installing Basic Collections
A collection is a set of parsers, scenarios, and log files for a specific service. Do not install everything indiscriminately: unnecessary scenarios increase noise and resource usage.
# Обновляем индекс Hub
sudo cscli hub update
# Устанавливаем сценарии Linux и SSH
sudo cscli collections install crowdsecurity/linux
sudo cscli collections install crowdsecurity/sshd
# Устанавливаем парсеры и сценарии Nginx
sudo cscli collections install crowdsecurity/nginx
# Проверяем установленные коллекции
sudo cscli collections list
Installing firewall-bouncer
For nftables, the firewall-bouncer package with the nftables backend is required. The package name may vary depending on the distribution and repository, so first find the available package.
# Ищем доступные варианты bouncer
apt-cache search crowdsec | grep -i bouncer
# Устанавливаем bouncer для nftables
sudo apt install -y crowdsec-firewall-bouncer-nftables
# Включаем применение решений на firewall
sudo systemctl enable --now crowdsec-firewall-bouncer
# Проверяем состояние bouncer
sudo systemctl status crowdsec-firewall-bouncer --no-pager
Installing Nginx
Nginx is needed for a clear access log and application proxying. If the reverse proxy is already implemented with Caddy, installing Nginx is not necessary; in that case, connect the collection for Caddy or the application log. We will keep Nginx for the main setup.
# Устанавливаем Nginx из системного репозитория
sudo apt install -y nginx
# Запускаем web-сервер и добавляем его в автозагрузку
sudo systemctl enable --now nginx
# Проверяем локальный HTTP-ответ
curl -I http://127.0.0.1
Installing Docker Engine
For production, use the official Docker Engine rather than the outdated docker.io package if you need current Compose plugin versions and fixes. Version 27/28 is suitable for a typical CrowdSec installation in 2026.
# Добавляем ключ официального репозитория Docker
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Подключаем репозиторий Docker для Debian 12
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" \
| sudo tee /etc/apt/sources.list.d/docker.list
# Устанавливаем Engine, CLI и Compose plugin
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Разрешаем deploy работать с Docker без sudo
sudo usermod -aG docker deploy
# Проверяем Docker и Compose
sudo docker version
sudo docker compose version
After adding the group, log in to SSH again. The docker group effectively grants root privileges, so do not add random users to it or mount the Docker socket inside untrusted containers.
7. Configuration and Verification
Connecting SSH and Nginx Logs
On Debian, SSH usually writes to journald or /var/log/auth.log. By default, Nginx uses /var/log/nginx/access.log and /var/log/nginx/error.log. View the active CrowdSec configuration: it will indicate which acquisition files have already been created by the package.
# Показываем источники логов CrowdSec
sudo cscli acquisitions list
# Проверяем наличие системных и Nginx-журналов
sudo ls -l /var/log/auth.log /var/log/nginx/access.log /var/log/nginx/error.log
# Читаем последние сообщения CrowdSec
sudo journalctl -u crowdsec -n 100 --no-pager
If the acquisition file has not been created, add a separate configuration. The format may differ slightly between versions, so always check the syntax by restarting the service after making changes.
# Создаём источник логов Nginx
sudo tee /etc/crowdsec/acquis.d/nginx.yaml >/dev/null <<'EOF'
filenames:
- /var/log/nginx/access.log
- /var/log/nginx/error.log
labels:
type: nginx
EOF
# Создаём источник системного auth.log
sudo tee /etc/crowdsec/acquis.d/sshd.yaml >/dev/null <<'EOF'
filenames:
- /var/log/auth.log
labels:
type: syslog
EOF
# Перезапускаем engine после изменения источников
sudo systemctl restart crowdsec
# Проверяем источники ещё раз
sudo cscli acquisitions list
Nginx Configuration
The site should proxy the application only through loopback or an internal Docker network. Do not publish the application port directly if access to it should go through Nginx.
# Создаём виртуальный хост для example.com
sudo tee /etc/nginx/sites-available/example.com >/dev/null <<'EOF'
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
access_log /var/log/nginx/example.access.log;
error_log /var/log/nginx/example.error.log warn;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
EOF
# Включаем сайт
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
# Удаляем дефолтный сайт при необходимости
sudo rm -f /etc/nginx/sites-enabled/default
# Проверяем конфигурацию и применяем её
sudo nginx -t
sudo systemctl reload nginx
In production, replace the domain with the real one and point the DNS A record to the VPS IPv4 address. For IPv6, add an AAAA record only after verifying that the firewall and application handle IPv6 correctly.
HTTPS via Caddy
If you do not want to manage certificates manually in Nginx, Caddy can serve as the sole reverse proxy. Do not run Caddy and Nginx simultaneously on ports 80 and 443. Below is a separate Docker Compose option: stop Nginx or assign it an internal port.
# Создаём каталог Caddy
sudo mkdir -p /opt/caddy/data /opt/caddy/config
sudo chown -R deploy:deploy /opt/caddy
# Переходим в каталог проекта
cd /opt/caddy
# Создаём файл с reverse proxy и автоматическим HTTPS
cat > Caddyfile <<'EOF'
example.com {
encode gzip zstd
reverse_proxy app:8080
header {
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
}
}
EOF
# Описываем Caddy и приложение в Compose
cat > compose.yaml <<'EOF'
services:
app:
image: nginx:1.27-alpine
restart: unless-stopped
expose:
- "8080"
caddy:
image: caddy:2.9-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./data:/data
- ./config:/config
depends_on:
- app
EOF
# Запускаем стек и проверяем контейнеры
docker compose up -d
docker compose ps
In a real project, replace the test application image with your own. Caddy will request a certificate automatically if DNS already points to the VPS and incoming connections on ports 80/443 are available. If Nginx remains the external reverse proxy, Caddy should listen only on an internal port, with TLS terminated in Nginx.
Collecting Docker Logs
CrowdSec should not read Docker's binary database directly. For a small server, it is simpler to configure Docker JSON files and direct them to acquisition. Limiting log size is essential; otherwise, one noisy container can fill the disk.
# Ограничиваем размер стандартных Docker-логов
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "5"
}
}
EOF
# Перезапускаем Docker; контейнеры могут кратко прерваться
sudo systemctl restart docker
# Проверяем конфигурацию Docker
docker info --format '{{.LoggingDriver}}'
The Docker collection can analyze events through a suitable parser, but the exact format depends on the CrowdSec and application versions. First install the collection and inspect its acquisition files:
# Обновляем Hub и устанавливаем коллекцию Docker
sudo cscli hub update
sudo cscli collections install crowdsecurity/docker
# Просматриваем файлы коллекции
sudo find /etc/crowdsec -type f | grep -i docker
# Перезапускаем CrowdSec после установки коллекции
sudo systemctl restart crowdsec
Registering a bouncer and Checking Decisions
During installation, the package usually creates a bouncer API key automatically. If the key was not created, generate it manually and add it to the bouncer configuration. Do not publish this key in Git or submit it to public issues.
# Смотрим зарегистрированные bouncer
sudo cscli bouncers list
# При необходимости создаём новый ключ
sudo cscli bouncers add firewall-local
# Проверяем локальные решения
sudo cscli decisions list
# Смотрим статистику чтения логов и срабатывания сценариев
sudo cscli metrics
Safe Blocking Test
Do not test a ban using your primary work IP: you could lose access. Use a temporary address, the provider console, or a local decision with a short duration. Delete the decision after the test.
# Добавляем тестовый адрес на одну минуту; замените TEST_IP
sudo cscli decisions add --ip TEST_IP --duration 1m --reason "manual-test"
# Проверяем, что решение появилось
sudo cscli decisions list
# Проверяем цепочки CrowdSec в nftables
sudo nft list ruleset | grep -i crowdsec -A 12
# Удаляем тестовое решение
sudo cscli decisions delete --ip TEST_IP
Checking SSH and HTTP
# Проверяем открытые TCP-порты на самом сервере
sudo ss -lntup
# Проверяем локальный Nginx
curl -I http://127.0.0.1
# Проверяем HTTPS с внешнего компьютера
curl -I https://example.com
# Проверяем сообщения bouncer
sudo journalctl -u crowdsec-firewall-bouncer -n 100 --no-pager
# Проверяем ошибки Nginx за последние строки
sudo tail -n 50 /var/log/nginx/error.log
Configuring Notifications
To start, it is sufficient to check metrics and systemd logs periodically. In production, it is useful to connect monitoring such as Prometheus node exporter, Uptime Kuma, Zabbix, or another tool already in use. Do not send complete log lines in notifications if they may contain tokens, URLs with secrets, or personal data.
8. Backups and Maintenance
What Needs to Be Saved
/etc/crowdsec/— acquisition, local settings, and scenarios you modified./etc/crowdsec/bouncers/— firewall-bouncer configuration./etc/ssh/,/etc/nginx/,/etc/nftables.conf, and the Caddy configuration.- Compose files, Docker secrets templates, and environment variables without exposing secrets in logs.
- Application data: databases, user uploads, certificates, and persistent volumes.
- The package list and configuration output, so the environment can be restored on a new VPS.
A Docker image should not be considered a data backup. The image can be downloaded again, but the volume contents, database, and keys cannot. For PostgreSQL, create a logical dump; for MySQL or MariaDB, use the corresponding dump tool before copying files.
PostgreSQL Dump Example
# Создаём каталог для локальных временных дампов
sudo install -d -m 700 /var/backups/postgres
# Выполняем сжатый дамп конкретной базы
sudo -u postgres pg_dump -Fc appdb \
> /var/backups/postgres/appdb-$(date +%F).dump
# Удаляем локальные дампы старше семи дней
sudo find /var/backups/postgres -type f -mtime +7 -delete
Restic to External S3
Backups should be stored outside the VPS: in S3-compatible object storage, on a separate server, or in two independent locations. Pass Restic secrets through an environment file with 600 permissions. The example uses S3-compatible storage; replace the URL and bucket with your own.
# Устанавливаем restic
sudo apt install -y restic
# Создаём файл секретов с закрытыми правами
sudo tee /root/.restic-env >/dev/null <<'EOF'
export AWS_ACCESS_KEY_ID='CHANGE_ME'
export AWS_SECRET_ACCESS_KEY='CHANGE_ME'
export RESTIC_REPOSITORY='s3:https://s3.example.net/server-backups'
export RESTIC_PASSWORD='CHANGE_ME_LONG_RANDOM_PASSWORD'
EOF
sudo chmod 600 /root/.restic-env
# Инициализируем репозиторий один раз
sudo bash -c 'source /root/.restic-env && restic init'
Do not include directories with temporary files and Docker cache in the archive. The Restic secrets file must not be backed up in the same form together with the archive: store the password in a secrets manager or in an offline copy.
# Создаём скрипт резервного копирования
sudo tee /usr/local/sbin/backup-server.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
source /root/.restic-env
restic backup \
/etc/crowdsec \
/etc/nginx \
/etc/ssh \
/etc/docker \
/etc/nftables.conf \
/opt \
/var/backups/postgres \
--tag "$(hostname)-$(date +%F)"
restic forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--prune
EOF
# Разрешаем запускать скрипт только root
sudo chmod 700 /usr/local/sbin/backup-server.sh
# Выполняем первый backup вручную
sudo /usr/local/sbin/backup-server.sh
# Проверяем список snapshots
sudo bash -c 'source /root/.restic-env && restic snapshots'
Cron and Restore Verification
# Запускаем backup ежедневно в 03:15
echo '15 3 * root /usr/local/sbin/backup-server.sh >> /var/log/backup-server.log 2>&1' \
| sudo tee /etc/cron.d/backup-server
# Проверяем структуру cron-файла
sudo chmod 644 /etc/cron.d/backup-server
sudo cat /etc/cron.d/backup-server
# Тестируем восстановление одного файла в отдельный каталог
sudo mkdir -p /tmp/restic-restore
sudo bash -c 'source /root/.restic-env && restic restore latest --target /tmp/restic-restore --include /etc/crowdsec/config.yaml'
A backup is considered functional only after restoration. Once a month, create a temporary VPS or a separate directory and verify that the configuration can be read, the database can be imported, and Docker Compose starts with the restored variables.
Updates
CrowdSec, the bouncer, and parsers can be updated without stopping user applications, but firewall changes must always be checked. Before updating, save the current decisions and configuration. For a single VPS, it is best to choose a short maintenance window, especially when updating the kernel, Docker, or Nginx.
# Сохраняем список установленных пакетов
dpkg-query -W -f='${binary:Package}\t${Version}\n' \
| sudo tee /var/backups/packages-$(date +%F).txt
# Обновляем пакеты
sudo apt update
sudo apt upgrade -y
# Обновляем коллекции CrowdSec из Hub
sudo cscli hub update
sudo cscli hub upgrade
# Проверяем сервисы после обновления
sudo systemctl --failed
sudo systemctl status crowdsec crowdsec-firewall-bouncer nginx --no-pager
For multiple servers, use a staged rollout: first update one test VPS, check the metrics and logs, and then update the rest. A rolling update is possible only if a second reverse proxy or load balancer is available. A single server without a standby node requires a standard maintenance window.
9. Troubleshooting and FAQ
Why doesn't CrowdSec see SSH events?
Check where sshd writes: journalctl -u ssh, /var/log/auth.log, or another file. Then run sudo cscli acquisitions list and make sure the source has the correct label. Check read permissions and restart CrowdSec. If journald is used without a file, configure acquisition for systemd according to the documentation for the installed version rather than adding a nonexistent path.
Decisions are created, but IPs are not blocked. What should I check?
First run sudo cscli bouncers list and make sure the bouncer is registered and has a recent last pull. Then check systemctl status crowdsec-firewall-bouncer and the presence of CrowdSec chains in nft list ruleset. A common cause is that the iptables version of the bouncer is installed on a system using nftables. Install the correct package and check the backend in the bouncer configuration.
What is the minimum suitable VPS configuration?
For a single SSH service and a small Nginx deployment, 1 vCPU, 1 GB of RAM, and 20–30 GB of SSD storage are technically sufficient. The practical minimum for Docker and several services is 2 vCPUs, 4 GB of RAM, and 60 GB of NVMe storage. You need a public IPv4 address, a stable network, and the ability to open TCP ports 22, 80, and 443. If you plan to run a database, CI builds, or heavy containers, aim for 8 GB of RAM or more.
What should I choose—VPS or dedicated for this task?
A VPS is sufficient for CrowdSec, SSH, Nginx, and several containers. A dedicated server is justified not by the security stack itself, but by high application load, a large amount of RAM, intensive disk I/O, or a requirement for physical isolation. If the VPS provider offers guaranteed resources, a separate disk, and backups, it will be easier and cheaper to maintain. Choose dedicated hardware for sustained load, not “just in case.”
Why is a Docker container accessible from the internet even though the port is not allowed in nftables?
Docker modifies forwarding and NAT rules, so a published port may bypass the expected filtering model. The best fix is not to publish the port externally: use expose and connect the container to the reverse proxy's internal network. If publishing is mandatory, add explicit rules to the Docker chains or use a separate firewall architecture, testing it on a staging server first.
How can I remove an erroneous block on my IP?
View the decisions with sudo cscli decisions list. Remove the specific address with sudo cscli decisions delete --ip YOUR_IP. If the address appears again, find the source in the metrics and logs. The cause may be an incorrectly configured reverse proxy: CrowdSec sees the load balancer's IP instead of the client's, or your monitoring generates too many requests. Configure trusted proxies only for known addresses and do not trust an arbitrary X-Forwarded-For header.
Can I keep fail2ban and CrowdSec running simultaneously?
Technically, yes, but identical SSH jails create duplication and complicate investigations. For a simple setup, leave CrowdSec as the sole source of blocks and disable fail2ban. Running both is justified if fail2ban protects a separate service for which there is no suitable CrowdSec parser. In that case, separate the jails, blocking periods, and firewall chains, then document which component is responsible for each type of event.
HTTPS is not being issued by Caddy. What should I check?
Make sure the DNS A and AAAA records point to the correct server and that ports 80 and 443 are accessible externally. If an AAAA record exists but IPv6 is not configured, the ACME client may connect to the wrong address: temporarily fix IPv6 or configure it correctly. Check the logs with docker compose logs caddy, make sure another process has not occupied the ports, and do not run an external Nginx and Caddy simultaneously on the same socket.
The disk is filling up quickly with logs. How can I fix this?
Check directory sizes with sudo du -xhd1 /var/log /var/lib/docker. Configure logrotate for Nginx, Docker limits max-size and max-file, as well as journald retention. Do not delete active logs manually: after rotation, send the service a reload. Then check that CrowdSec is reading the new file and has not lost its position after rotation.
10. Conclusions and Next Steps
The VPS now runs the CrowdSec engine, scenarios for SSH and Nginx, the firewall-bouncer on nftables, and controlled Docker services. SSH access is restricted to keys, web traffic passes through a reverse proxy, and the configurations and data are prepared for backup.
The next step is to connect metrics monitoring and regularly conduct a recovery test. As the load grows, move databases and applications to separate nodes, and when using multiple VPSs, use centralized logs and a unified update process.
- Check for false positives on real traffic and add exceptions only for trusted networks.
- Configure staged updates, external backup, and a monthly disaster recovery test.
- For multiple domains or servers, add an external WAF/CDN without replacing local CrowdSec protection.