Fail2ban Behind Reverse Proxy

Assumptions, Prerequisites, Prior Set Up

This guide is for when the reverse proxy & FoundryVTT server are both on separate machines

This guide builds upon the prior one and details how to setup fail2ban to block bad bots, automated login attempts, and repeated connection attempts that are likely malicious, when your server is behind a reverse proxy. 

If you haven't taken a look already my prior post detailed how to go about setting up fail2ban rules.
https://blog.iterlyze.com/2024/11/fail2ban-foundryvtt.html 

This guide assumes you are using a Debian 12 server (or Ubuntu) and have a basic idea of how to use a Linux shell. Some other assumptions that won't affect setting up fail2ban include that you are using Nginx as the reverse proxy, nftables as your system's firewall frontend, and that you are forwarding traffic to a FoundryVTT server.

Make sure the reverse proxy and the server it sends traffic to is working before trying to set up fail2ban. If you are setting up FoundryVTT & a reverse proxy for the first time the official FoundryVTT site has some great step by step guides you can follow. You can use Apache (widely used), Nginx (fast & efficient), or Caddy (easy to use).
Recommended Linux Installation Guide | Foundry VTT Community Wiki

Caddy Proxy Server | Foundry Virtual Tabletop
Nginx Proxy Server | Foundry Virtual Tabletop
Apache Proxy Server | Foundry Virtual Tabletop

If you have trouble with the Linux commands the following blog post may help https://blog.iterlyze.com/2020/10/i-recently-made-quick-draft-of-useful.html
(If you need help with the basics i'm happy to answer questions)

Introduction

These steps should work for any reverse proxy & log file you want fail2ban to take a look at. The issue when using a reverse proxy between your servers and the internet is that fail2ban and your firewall primarily block at the network/transport layer while the ip address of the connecting device is encapsulated by your reverse proxy in the application layer. While there are some features you can use for looking at strings in the application layer, if you are using https you won't be able (unless you use a different setup). The solution is to have fail2ban operate on your reverse proxy instead of the server. I'll include the commands for setting up fail2ban at the end (Starting on step 8) with less explanation than the previous post. If you run into problems, start with the simple setup then slowly add to it.

Login to the root account, with su or use sudo before running the commands below.

Step 1) Install needed software

On the server (FoundryVTT) install the needed software. We will be using the lsyncd which in turn uses rsync to send/update logfile changes to our reverse proxy. lsyncd allows for finding changes in files in seconds from the time the files have been changed.

apt-get update; 
apt-get install -y lsyncd;

On the reverse proxy install fail2ban and a firewall (If you want to use nftables as your firewall).

apt-get update;
apt-get install fail2ban nftables;

Step 2) Create directory on Reverse Proxy

On the reverse proxy create a directory to store your log files

mkdir -p ~/foundryLogs;

Step 3) Create Directory & Configs On Server with Logs (FoundryVTT)

mkdir /var/log/lsyncd; 
touch /var/log/lsyncd/lsyncd.{log,status};
mkdir /etc/lsyncd;

In order for lsyncd to work correctly we need to create a location for it to store it's own log files. (idk why when you install the package it doesn't just make this for you). The first command creates a directory in the /var/log folder. The second command creates two files lsyncd.log and lsyncd.status in the /var/log/lsyncd/ folder. The third command creates a directory in the /etc/ folder where you will place the lsyncd configuration called named lsyncd.conf.lua. This file is written using valid Lua syntax, so if you know that programming language it might make things a little bit easier for you to write without error. (Knowing Lua is not needed, I don't know Lua)

Continue with creating the configuration file. You will need to make sure the directories match the ones you have created, that you use absolute paths when defining them, and that the usernames match for the source (FoundryVTT) and destination (rsync section) You might like to note that you can easily find the IP address of each machine by logging in and running hostname -I 
(Copy and pasting this won't work, check targetdir matches fail2ban in your config)

nano /etc/lsyncd/lsyncd.conf.lua    

settings { 	logfile = "/var/log/lsyncd/lsyncd.log",
		statusFile = "/var/log/lsyncd/lsyncd.status",
		inotifyMode = "Modify" }

sync { default.rsyncssh,
	delay = 1,
	source = "/home/username/foundryuserdata/Logs/",
	host = "192.168.1.xxx",
	targetdir = "/home/username/foundryLogs/",
	exclude= { 'error.*','diagnostics.*','news.*','*.json', 'debug.log.*' },
	rsync = { rsh = "/usr/bin/ssh -l username -i /home/username/.ssh/id_ed25519 -o UserKnownHostsFile=/home/username/.ssh/known_hosts" }
}
The first part of the lsyncd.conf.lua file contains the settings function. You define where you want its log files located. In this case in the folders that we created above. Of particular note we will be setting the inotifyMode to Modify. It appears that foundryVTT doesn't CloseWrite, so we need lsyncd to specifically look for modifications to the file instead. This is how we are detecting near realtime changes to the log.

The second part of the lsyncd.conf.lua file contains the sync function which configures how, where, when, who, and what for sending the log file to the reverse proxy.

(How we send changes)
We want the logfile to be sent securely so we will be using rsyncssh by default. 

(When we send changes)
"delay" is how quickly we want to detect that changes have been made to the file. You can set this slower if you want depending partly on how sensitive your fail2ban setup is. 1 is quick, 5 is standard, 10 is acceptable normally. If you have a low resource system you might set the delay to a higher number if needed. 

(Where we send changes from)
"source" is where lsyncd is getting its log files from. In this case the server (FoundryVTT). 

(Where we send changes to)
"host" is the network address the logfiles are being sent to (Reverse Proxy). 

"targetdir" is directory on the host (Reverse Proxy) that the log files are being sent to. 

(What we don't send)
"exclude" is a list of patterns for files that we DON'T want to send. The various versions of FoundryVTT seem to have various different log names, so you might change this list depending on what version that you are using. What's listed should work for v10 and maybe v12.

(Specific Whom & How we send changes)
"rcync" is a set of options for who & how we are sending the log files. This is for configuration of the Rsync utility. Since we really don't need the root account for accessing the log files we should use the useraccount that we are using for foundry. 

We tell lsyncd to tell rsync where the binary for the secure shell (ssh) is.

-l to set the username we will be login to on the reverse proxy. 
-i is to tell rsync where the key for remote login is for the reverse proxy. (more info when we make it)
-o tells rsync where the list of known hosts is.

Step 4) Create SSH keys on both your server (FoundryVTT) and your reverse proxy.

This is different than configuring certificates and ssl for using https. The technology is similar but in this case it's only for keeping your internal communications secure. Start by running these commands on your server (FoundryVTT). Once again you can use hostname -I to find your ip address quickly.

ssh-keygen -t ed25519
ssh-copy-id username@reverse_proxy_address 
Then restart sshd on the reverse proxy and attempt to login.
ssh username@reverse_proxy_address 

Then run the commands again on your reverse proxy.

ssh-keygen -t ed25519
ssh-copy-id username@FoundryVTT_address 
Then restart sshd on the FoundryVTT server and attempt to login.
ssh username@FoundryVTT_Address 

The from= goes on the reverse proxy in /home/username/.ssh/authorized_keys. ssh-copy-id added one line there that starts with ssh-ed25519 AAAA…. Put the options at the very start of that line, followed by a space:

from="<FoundryVTT_ip>",restrict ssh-ed25519 AAAA...rest-of-key... username@foundryhost

restrict turns off port, agent and X11 forwarding and terminal allocation, which rsync doesn't need.

Root vs. user paths. The post says to log in as root, but Step 4 must be run as the regular user. If it's run as root, ssh-keygen writes the key to /root/.ssh/, while your lsyncd config points to /home/username/.ssh/id_ed25519. mkdir -p ~/foundryLogs in Step 2 has the same problem.

Step 5) Test that lsyncd is working

On your host server (FoundryVTT)
systemctl enable lsyncd
service lsyncd start
systemctl status lsyncd

Also check that the files have been successfully transferred over.
If they haven't, check your firewall configuration, or check again after the next step.

Step 6) Configure your firewall

This is a basic configuration for the firewall on the reverse proxy, skip this step if you use a different firewall. This is an ipv4 setup it won't work with ipv6.

nano /etc/nftables.conf 
/usr/sbin/nft list ruleset 
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
     chain input {
          type filter hook input priority 2; policy accept;
          ct state established, related accept
          ct state invalid drop
          iif lo counter accept
          ct state new tcp dport 22 accept
          ct state new tcp dport 80 accept
          ct state new tcp dport 443 accept
          ct state new tcp dport 30000 accept
          ip saddr 192.168.1.xxx ct state new tcp dport 30000 accept drop } chain forward { type filter hook forward priority filter; policy drop; } chain output { type filter hook output priority filter; policy accept; } }
If you want to restrict the Foundry VTT server only to accept traffic from the proxy use the orange line, otherwise use the striked through text. (I personally would sneak into it and bypass the reverse proxy, but that leaves a little bit of a security hole)

There is a lot going on here. First line is what binary interprets this file. (#! is pronounced Crunch Bang. Fight me if you disagree :p [I accept pound bang I guess too...] ) 
Flush Ruleset
Clears out any lingering firewall rules.
table inet filter {
table inet filter - Create a table. Multiple tables are supported. inet means it should work on both IPv4 and IPv6.
     chain input {
Rules are defined at various places you can hook them to as network traffic is being processed. Input is right before any of our applications see things and is the appropriate place for most general firewall rules. Forward, and output are other places you can hook into.
type filter hook input priority 2; policy accept;
This is the first line of our filters, it defines the type, where it hooks, the priority (e.g. the order it is processed compared to everything else), and the default policy action to take after going through everything in that particular chain.
         ct state established, related accept
Allow network connections from already established connections. (e.g. if you (outgoing) tried to talk to a webpage, it should be allowed to respond).
          ct state invalid drop
If the network traffic is sus and doesn't look like anything that should be coming in, don't let it in.
          iif lo accept
Let network traffic from yourself (localhost) to yourself.
          ct state new tcp dport 22 accept
Allow ssh if it's a new connection. (If it isn't new then the rule above for state will let it in)
          ct state new tcp dport 80 accept
Allow new http connections. (Technically its a new state, not connection, but i'm simplifying here. If it isn't new we got another rule for that above)
          ct state new tcp dport 443 accept
This rule is for https, as should be expected just for initiating https.
          ct state new tcp dport 30000 accept
This is our rule to allow connections to FoundryVTT. If you changed the default port you need to change this rule to match what you changed it to.
          drop
Anything else shouldn't be let in. (You might need to add additional ports, say for example if your internal ports don't match what the reverse proxy is accepting)

Step 7) Restart your firewall

systemctl restart nftables

OR reload the rules from your config

/usr/sbin/nft -f /etc/nftables.conf

As a note any time you restart or reload your firewall you also need to do so with fail2ban. Here is a great blog post about how to do that automagically everytime. Using systemd to bind fail2ban to nftables < System | The Art of Web

You can see your currently loaded firewall rules with the following command.

/usr/sbin/nft list ruleset

Test everything above by trying to browse to your FoundryVTT instance.
Assuming that it works, below is a quick config for setting up fail2ban. (If you haven't already read the other post)
The following steps should be done completely on the Reverse Proxy.

Step 8) Create Local Configs

 touch /etc/fail2ban/jail.local; touch /etc/fail2ban/jail.d/local.conf

Step 9) Set your settings

nano /etc/fail2ban/jail.local
[DEFAULT]
findtime = 2h
maxretry = 5
bantime.increment  = true
bantime.rndtime    = 1024
banaction_allports = nftables-allports
banaction          = nftables-allports

[nginx-botsearch]
maxretry = 2

[foundryvtt]
findtime = 3m
maxretry = 3
bantime  = 60
port     = 80
logpath  = /home/username/foundryuserdata/Logs/debug.log
backend  = polling
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24

nano /etc/fail2ban/jail.d/local.conf
[sshd]
enabled = true

[nginx-bad-request]
enabled = true

[nginx-botsearch]
enabled = true

[nginx-http-auth]
enabled = true

[foundryvtt]
enabled = true

echo "sshd_backend = systemd" >> /etc/fail2ban/paths-debian.conf 

Added some settings that aren't included in the other post as a hint to more things you might want to consider using. Assuming Nginx as the reverse proxy.

Claude suggests: 

tested this fix: logpath = /home/foundry/foundryuserdata/Logs/debug.*.log

Fail2ban expands the * only when the jail starts. In my test, a new day's file wasn't watched until I ran fail2ban-client reload foundryvtt, and then it was. A daily cron entry handles that. Put it in /etc/cron.d/fail2ban-foundry:

5 0 * * * root /usr/bin/fail2ban-client reload foundryvtt

Step 10) Create a regular expression to detect log entries

nano /etc/fail2ban/filter.d/foundryvtt.conf

[Definition]

failregex = ^\{\"ip\":\"<HOST>\",\"level\":\"warn\",\"message\":\"((Administrator authentication failed for session [a-zA-Z0-9]{24}; invalid password)|(User authentication failed for user [a-zA-Z0-9]{1,50}; invalid password\",\"session\":\"[a-zA-Z0-9]{24}))\",\"status\":40[13],\"timestamp\":\"\"}$

Step 11) Test 

systemctl restart nftables; systemctl restart fail2ban 

Try to get yourself banned, and then issue the command to unban all ip's fail2ban-client unban --all 


If you notice, there are some discrepancies in the settings i've listed, it means you can try your own values and customize it to what appears to work best for your system.

Conclusion

Well there we have it. Hopefully by now you've been able to setup fail2ban when you are using a reverse proxy. At the time of this post (2024) there seem to be some issues with fail2ban and the default sshd jail on debian you can try this sudo echo "sshd_backend = systemd" >> /etc/fail2ban/paths-debian.conf or echo "sshd_backend = systemd" | sudo tee -a /etc/fail2ban/paths-debian.conf Good luck, if you have problems feel free to leave a comment.

*Added some updates after having an AI review the post in orange.

"The post assumes Debian 12, which is no longer current stable. As of June 11, 2026, Debian 12 moved into Long Term Support, which runs until June 30, 2028. Debian 13 configures Fail2Ban to use the systemd journal and nftables by default and packages version 1.1.0. That means the sshd workaround isn't needed on Debian 13." --My friendly LLM 

Comments

Popular posts from this blog

Fail2ban Rules for Foundry VTT

Proxmox Link Aggregation Using balance-alb Windows vs Linux