Blocking an address and locking an account are two different counter mechanisms, and they fail in opposite directions. An address ban stops one source and leaves the targeted account reachable from anywhere else. An account lock stops that identity everywhere, including from the legitimate user's own laptop. Teams often deploy one while describing the other, then discover the difference during an incident when the wrong thing is protected.

For a VPN that delegates authentication to PAM, the counter is whatever the PAM stack reaches, not whatever the listener logged. Separating those two is the first useful step.

Two counters, two different victims

A rate limiter watches a source and asks how many attempts came from it recently. A lockout watches an identity and asks how many failures that account has accumulated. The first protects availability of the service; the second protects the credential. Neither substitutes for the other, and a deployment that only has the first has not protected the account at all.

That is why "we block the IP after five failures" is not an answer to a credential-stuffing report. The attacker rotates sources, each one gets a small number of attempts, and the account is never counted. Conversely, a lockout policy without any source control lets an attacker keep the account locked indefinitely by failing on purpose, which is a denial of service that costs them nothing.

The two controls also have different recovery stories. An address ban expires on a timer that nobody has to think about. An account lock may need an administrator, a documented waiting period or a self-service reset, and each of those is a policy decision that has to exist before the first lockout happens.

Find the store that already owns the count

Before adding anything, find out whether a failure store already exists. A local Linux account, a directory service and an external identity provider each keep their own tally, and a PAM module added in front of them creates a second one rather than extending the first.

grep -rnE 'faillock|pam_tally|pam_unix' /etc/pam.d/
ls -l /etc/pam.d/ | head -20

Read the service file the VPN actually names, then follow its include lines. On many distributions the VPN service includes a shared common file, which means the tally you are about to tune also applies to SSH and to local logins. That may be acceptable, and it may be the opposite of what the change was meant to do.

If the directory already enforces a lockout, the useful question is how the PAM conversation reaches that decision and how the VPN reports it. Adding a local count on top does not become the directory's counter, and the two policies can disagree about when the account unlocks.

pam_faillock has three call positions, not one

The module separates three operations that a single copied line tends to blur: checking whether an account is already locked, recording an authentication failure, and clearing the failure history after a success. Its manual documents those as distinct call positions, and the stack decides which of them run and in what order.

That is the part a migration from an older tally module usually gets wrong. Copying a line from a working pam_tally2 example into a pam_faillock stack is not a rename; the documented setup differs, and a misplaced call can produce a stack that records failures without ever checking them, or checks them at a point the authentication path never reaches.

faillock --user alice
faillock --user alice --reset

The inspection utility is the fastest way to see whether the counter is real: it reports the recorded failures for an account and can clear them. If a controlled failure does not change that output, the call position is wrong regardless of how the configuration reads.

A dedicated service name is not isolation

Giving the VPN its own PAM service file makes the intended policy visible, and it does not by itself confine the effect. Any include of a shared file pulls in whatever that file does, including tallies that apply system-wide.

Decide explicitly whether the lockout is meant to cover only VPN access or every service that authenticates the same account. Both answers are defensible; the failure mode is believing you chose one while the stack implements the other. Write the answer down next to the service file so the next change does not silently reverse it.

Then account for the cost. A lockout can be triggered by an attacker, and the user who suffers is the one whose account it is. Threshold, waiting period and the administrative recovery path are all part of the policy, and the recovery path must not depend on the VPN being reconfigured during an incident.

Accept the sequence, not a single rejection

Test with designated identities under an approved plan, and test a chain rather than a single event. A valid account should authenticate; a deliberate bad password should move the counter; the configured lock should then take effect; and the documented recovery should give access back.

faillock --user test-vpn-01
journalctl -u openvpn-server@lab -n 30 --no-pager | grep -i 'auth\|pam'

Check the success behaviour as carefully as the failure behaviour. If a successful login does not clear the history, an account will lock after a slow accumulation of ordinary mistakes. If it clears everything from every service, the counter is weaker than the policy claims.

Finally, stop unattended clients before you declare victory. A saved credential that retries in the background will recreate the failures minutes after a recovery and lock the account again, which looks like a broken recovery path rather than a misconfigured client. Source-based rate limiting can stay in place as an additional control, but it is not the account policy and should not be described as one.

Pennadoù liammet