You copied an OpenVPN jail from a community post, restarted fail2ban, and no address is ever banned. The reason is almost always the log format: Access Server does not write the authentication failure lines a stock filter expects. Before you touch the filter, confirm the records are there at all, because a pattern cannot match what the service never logged.

This is the runbook I follow on an Access Server host: confirm the record shape, write one anchored expression, test it against the real log, then split the jails by the surface being attacked and verify that the ban reaches the firewall.

Confirm the failure record exists

Access Server writes its own structured lines to /var/log/openvpnas.log, and the client address lives in a quoted real_ip field rather than at the start of a syslog line. Grep for a failed login you know happened before writing any pattern:

grep -i 'LOG ERR' /var/log/openvpnas.log | tail -20

If nothing shows up, the log level is the next suspect. Access Server has a log-database debug switch in /usr/local/openvpn_as/etc/as.conf (DEBUG_LOGDB=1), and the service has to be restarted after changing it. Treat that switch as a diagnostic setting to be turned back down once you have your pattern, not as a permanent configuration.

While you are in the file, note which surfaces write into it. Client portal failures, admin interface failures and tunnel authentication failures land in the same file with different markers, and that detail decides the jail layout later.

Anchor one filter on real_ip

Keep the expression narrow. A filter that claims any line containing an IP address will ban people for ordinary disconnects, and on a busy server the only symptom is users complaining that they were locked out for closing a laptop. Claim the error record and capture the quoted address:

# /etc/fail2ban/filter.d/openvpnaccess.local
[Definition]
failregex = ^ .*LOG ERR.*"real_ip": "<HOST>".*
ignoreregex =

The leading space is not cosmetic. Access Server indents its records, and anchoring on it keeps the pattern from also claiming lines emitted by unrelated services that write to the same file. If your records carry a leading timestamp instead, match that prefix, but anchor on something that is genuinely present in every record you intend to claim.

When you need per-surface precision, narrow further by requiring the surface marker (WEB_CLIENT, WEB_ADMIN or VPN) in the same line. One filter with three patterns is easier to audit than three filters that differ by one word.

Test the expression before enabling a jail

Never enable a filter you have not run against the production log. The documented test passes the filter twice, once to load and once to apply, and the missed-line report is the part worth reading:

fail2ban-regex --print-all-missed /var/log/openvpnas.log \
  /etc/fail2ban/filter.d/openvpnaccess.local \
  /etc/fail2ban/filter.d/openvpnaccess.local > missed.txt

Compare the match count with the failures you verified in step one. Zero matches means the pattern is wrong; matches on lines that are not authentication failures means it is too broad. Read missed.txt until you can explain every line you chose not to claim.

Do this test on a copy if the log is large, and re-run it after any Access Server upgrade. The log format is the interface you are matching against, and it is not a stable API.

Split jails by surface, not by daemon

One Access Server presents several authentication surfaces: client and admin web interfaces on 943/tcp, and the tunnel on 1194/udp or 443/tcp in most deployments. A single jail covering all of them lets tunnel guessing trips ban an address that is also your admin path, and it hides which surface is actually under attack. Give each surface its own jail with its own port and thresholds:

# /etc/fail2ban/jail.d/openvpnaccess.local
[openvpnas-web-client]
enabled  = true
port     = 943
protocol = tcp
filter   = openvpnaccess
logpath  = /var/log/openvpnas.log
maxretry = 3
findtime = 10m
bantime  = 1h
[openvpnas-tunnel]
enabled  = true
port     = 443
protocol = udp
filter   = openvpnaccess
logpath  = /var/log/openvpnas.log
maxretry = 5
findtime = 10m
bantime  = 1h

Start with small counters and a short ban, then raise them once you have watched a week of real traffic. A generous threshold on a shared office address punishes every user behind one NAT, and an aggressive ban on the tunnel surface disconnects legitimate clients whose stored credentials expired overnight.

Verify the ban, the backend and the collateral

After a restart, the status output is the fastest confirmation that the jail exists and is counting:

fail2ban-client status
fail2ban-client status openvpnas-tunnel

Then confirm the ban reaches the firewall you actually run, because a jail configured for a backend that nothing enforces protects nothing. With the iptables action the addresses appear in a chain named after the jail (f2b-openvpnas-tunnel); with the nftables action they land in a set in fail2ban own table. Check the one your host uses rather than assuming.

Two collateral checks matter more than the ban itself. First, verify that real_ip is the client address and not a load balancer or reverse proxy; behind a proxy every ban lands on the proxy and takes the whole service down with it. Second, write the unban procedure down, since the address that trips the threshold at 02:00 usually belongs to the on-call engineer: fail2ban-client set openvpnas-tunnel unbanip 203.0.113.24.

Keep the boundary in mind while tuning. Fail2ban is a rate limiter on one authentication surface, not an access control layer. It does not replace MFA, per-account restrictions, or keeping the admin interface off the public internet, and Access Server has its own autoban settings that are worth checking before you stack two rate limiters on the same port.

Аффингæй хæстæг статьялтæ