# Fail2Ban Fixes – Deployment Notes for PBX Developers

> **Date:** 2026-06-25  
> **Applies to:** PBX deployments with built-in fail2ban + nftables geo-firewall  
> **Goal:** Make fail2ban actually block attackers on HTTP-only/internal PBXs as well as HTTPS-hosted PBXs.

---

## 1. Executive Summary

Fail2ban was running and appeared healthy on affected PBXs, but it was **not enforcing any bans**. The service was active, but three separate configuration bugs meant:

1. Web-login failures were never read (wrong backend).
2. SIP scanner bans were written to an unused `ipset` instead of nftables.
3. Portal brute-force detection was watching the HTTPS access log even on HTTP-only PBXs.

This document describes the exact changes needed in the PBX deployment/upgrade tooling so these fixes are pushed to all clients automatically.

---

## 2. Root Causes

### 2.1 `pbx-weblogin` jail reads systemd journal instead of the Laravel log file

**Current broken config (`/etc/fail2ban/jail.d/pbx-weblogin.conf`):**

```ini
[pbx-weblogin]
enabled   = true
port      = 80,443
filter    = pbx-weblogin
logpath   = /var/www/html/storage/logs/auth-failures.log
banaction = nftables-multiport
maxretry  = 5
findtime  = 600
bantime   = 3600
```

`[DEFAULT]` in `/etc/fail2ban/jail.local` sets `backend = systemd`. Because `pbx-weblogin` does **not** override it, the jail watches the systemd journal with **no** `journalmatch`. The real web-login failures are written to `/var/www/html/storage/logs/auth-failures.log` and are ignored.

**Fix:** add `backend = polling` to the jail.

```ini
[pbx-weblogin]
enabled   = true
port      = 80,443
filter    = pbx-weblogin
logpath   = /var/www/html/storage/logs/auth-failures.log
banaction = nftables-multiport
backend   = polling          # <-- ADD THIS
maxretry  = 5
findtime  = 600
bantime   = 3600
```

### 2.2 `asterisk-sipvicious` uses an `ipset` that is never referenced by nftables

**Current broken config (`/etc/fail2ban/jail.d/asterisk-sipvicious.local`):**

```ini
[asterisk-sipvicious]
enabled  = true
logpath  = /var/log/asterisk/full
           /var/log/asterisk/messages
backend  = auto
findtime = 300
maxretry = 1
bantime  = 604800
action   = ipset-pbx          # <-- WRONG on nftables-only PBXs
```

The `ipset-pbx` action adds the banned IP to an `ipset` named `pbx_blacklist`. On these PBXs the firewall is **nftables**, not iptables. The `pbx_blacklist` ipset has **zero references** in `nft list ruleset` and `iptables-save`, so banned IPs are stored but never blocked.

**Fix:** remove the `action` override and set the correct SIP port so the default nftables action is used.

```ini
[asterisk-sipvicious]
enabled  = true
logpath  = /var/log/asterisk/full
           /var/log/asterisk/messages
backend  = auto
findtime = 300
maxretry = 1
bantime  = 604800
port     = 5060,5061          # <-- REPLACE action = ipset-pbx with this
```

Because `[DEFAULT]` already sets `banaction = nftables-multiport`, the jail will now use nftables and block ports 5060/5061.

### 2.3 `pbx-portal` always watches the HTTPS access log

**Current broken config (`/etc/fail2ban/jail.d/pbx-portal.local`):**

```ini
[pbx-portal]
enabled  = true
backend  = polling
port     = http,https
filter   = pbx-portal
logpath  = /var/log/apache2/pbx-ssl-access.log   # <-- WRONG for HTTP-only PBXs
maxretry = 5
findtime = 300
bantime  = 3600
```

The filter (`/etc/fail2ban/filter.d/pbx-portal.conf`) looks for failed `POST /login` requests:

```ini
failregex = ^<HOST> .* "POST /login HTTP/.*" 302 .* "https?://[^/]+/login"
            ^<HOST> .* "POST /login HTTP/.*" 422
```

For **hosted HTTPS clients**, the PBX vhost logs to `/var/log/apache2/pbx-ssl-access.log` and the filter matches.  
For **internal HTTP-only clients** (behind a firewall, desktop phones, no public SSL), the PBX vhost logs to `/var/log/apache2/access.log`. The SSL log is **0 bytes**, so `pbx-portal` never sees any failures.

**Fix:** the deployment must choose the correct log path based on whether the PBX is deployed with SSL.

---

## 3. Solving the HTTP vs HTTPS Mixed Environment

You have two client types:

| Client type | Web protocol | Apache log file that contains `POST /login` | Current jail logpath | Status |
|---|---|---|---|---|
| Hosted / public | HTTPS | `/var/log/apache2/pbx-ssl-access.log` | `/var/log/apache2/pbx-ssl-access.log` | ✅ Working |
| Internal / behind firewall | HTTP | `/var/log/apache2/access.log` | `/var/log/apache2/pbx-ssl-access.log` | ❌ Broken (file is empty) |

### Recommended fix (option A): single dedicated log for portal traffic

The cleanest solution is to configure Apache to log **all** portal login requests to one file regardless of HTTP or HTTPS. Then the fail2ban jail always watches that file.

Example Apache snippet for both HTTP and HTTPS vhosts:

```apache
CustomLog /var/log/apache2/pbx-portal-access.log combined env=portal_login
```

Or use a dedicated `access.log` that both vhosts write to. Then update the jail once:

```ini
[pbx-portal]
enabled  = true
backend  = polling
port     = http,https
filter   = pbx-portal
logpath  = /var/log/apache2/pbx-portal-access.log   # <-- single file for both HTTP and HTTPS
maxretry = 5
findtime = 300
bantime  = 3600
```

### Alternative fix (option B): conditional logpath per deployment mode

If you cannot combine logs, generate the jail config during deployment based on the SSL mode selected by the installer:

- **LAN mode / no SSL** (`pbx-setup-client.sh --lan-mode`):
  ```ini
  logpath = /var/log/apache2/access.log
  ```
- **Public mode / with SSL** (`pbx-setup-client.sh`):
  ```ini
  logpath = /var/log/apache2/pbx-ssl-access.log
  ```

Do not hardcode either path in the static template that ships to both types of clients.

### Important: verify the HTTP status code

On the inspected PBX, `POST /login` on HTTP returned status `419` (Laravel "Page Expired"), not `302` or `422`. The current filter will not match `419`.

Before rolling out, test a failed portal login on both HTTP and HTTPS PBXs and update the filter if needed:

```ini
# /etc/fail2ban/filter.d/pbx-portal.conf
failregex = ^<HOST> .* "POST /login HTTP/.*" 302 .* "https?://[^/]+/login"
            ^<HOST> .* "POST /login HTTP/.*" 422
            ^<HOST> .* "POST /login HTTP/.*" 419   # <-- add if your PBX returns 419
```

Use `fail2ban-regex` to test:

```bash
fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/pbx-portal.conf
```

---

## 4. Summary of Required Template Changes

Update the PBX deployment templates (or `php artisan pbx:deploy` logic) that write these files:

| File | Change |
|---|---|
| `/etc/fail2ban/jail.d/pbx-weblogin.conf` | Add `backend = polling` |
| `/etc/fail2ban/jail.d/asterisk-sipvicious.local` | Remove `action = ipset-pbx`, add `port = 5060,5061` |
| `/etc/fail2ban/jail.d/pbx-portal.local` | Set `logpath` to the correct HTTP/HTTPS log (or a shared log) |
| `/etc/fail2ban/filter.d/pbx-portal.conf` | Add `419` if HTTP failed logins return that status |

After deployment, always restart fail2ban (not just reload) when an action type changes:

```bash
fail2ban-server -t && systemctl restart fail2ban
```

---

## 5. Verification Steps After Deployment

Run these on a test PBX after applying the changes:

```bash
# 1. Config is valid
fail2ban-server -t

# 2. Service is running
systemctl status fail2ban --no-pager

# 3. Jails are watching the right files
fail2ban-client status pbx-weblogin
fail2ban-client status pbx-portal
fail2ban-client status asterisk-sipvicious
```

Expected output:

- `pbx-weblogin` → `File list: /var/www/html/storage/logs/auth-failures.log`
- `pbx-portal` → `File list: /var/log/apache2/access.log` (HTTP) or `/var/log/apache2/pbx-ssl-access.log` (HTTPS)
- `asterisk-sipvicious` → `File list: /var/log/asterisk/full /var/log/asterisk/messages`

### Functional test

Use a documentation/testing IP (e.g. `203.0.113.50`):

```bash
fail2ban-client set pbx-weblogin banip 203.0.113.50
nft list table inet f2b-table   # should show the IP in addr-set-pbx-weblogin
fail2ban-client set pbx-weblogin unbanip 203.0.113.50

fail2ban-client set asterisk-sipvicious banip 203.0.113.50
nft list table inet f2b-table   # should show the IP in addr-set-asterisk-sipvicious
fail2ban-client set asterisk-sipvicious unbanip 203.0.113.50
```

For `pbx-portal`, first generate a real failed login and check that `fail2ban-client status pbx-portal` shows the failure and then a ban.

---

## 6. Rollback

Backups were created on the two manually fixed PBXs with names like:

```text
/etc/fail2ban/jail.d/pbx-weblogin.conf.bak.20260625080044
/etc/fail2ban/jail.d/asterisk-sipvicious.local.bak.20260625080044
/etc/fail2ban/jail.d/pbx-portal.local.bak.20260625080044
```

To roll back on a single PBX:

```bash
cp /etc/fail2ban/jail.d/pbx-weblogin.conf.bak.YYYYMMDDhhmmss /etc/fail2ban/jail.d/pbx-weblogin.conf
cp /etc/fail2ban/jail.d/asterisk-sipvicious.local.bak.YYYYMMDDhhmmss /etc/fail2ban/jail.d/asterisk-sipvicious.local
cp /etc/fail2ban/jail.d/pbx-portal.local.bak.YYYYMMDDhhmmss /etc/fail2ban/jail.d/pbx-portal.local
systemctl restart fail2ban
```

---

## 7. Files to Ship in the Next Release

- `/etc/fail2ban/jail.d/pbx-weblogin.conf`
- `/etc/fail2ban/jail.d/asterisk-sipvicious.local`
- `/etc/fail2ban/jail.d/pbx-portal.local`
- `/etc/fail2ban/filter.d/pbx-portal.conf` (only if the `419` status is confirmed)

Do not change the PBX application code. These are fail2ban/Apache configuration changes only.
