Can't Message Users Here, Reaching Out Via Thread

Since I seem to be unable to message any other users on this forum…I’m pinging @Austin_Kirsch to ask about a follow-up on RemoteAddr and Caddy v2 - help needed - #20 by Austin_Kirsch

Any chance you’ve had some time to work on a wiki/guide for this? :slight_smile:

Unfortunately I haven’t yet but I can put it back on my radar.

In the meantime, what issues are you having specifically?

1 Like

Mostly I was never able to find out a good way to get Caddy to actually log the real IP when behind Cloudflare so fail2ban could action it, so I’m curious to see whatever solution you came up with to just leverage the Cloudflare API to let Cloudflare act as fail2ban, basically.

So I use my realip module still but have a special fail2ban action that leverages the Cloudflare API.

My server is down right now so I cant access my files but when i get back up and running I’ll post them. Hopefully tonight but may be tomorrow.


I appreciate it. I’ll add the realip module in the meantime. :slight_smile:

Edit: as a general point of feedback, would it be better for this section to leverage the Cloudflare API for this so you’re never using out of date IP blocks and potentially accidentally blocking a lot of people (or yourself!)?

Edit 2: though…I guess if you did that on every request, that could get rather spammy…

yes absolutely that would be better. I plan in the (somewhat) near future to issue a GET request to and to pull the latest data via the provisioning configuration. Since, to my knowledge, provisioning is only done on module startup it should only pull this data when the module is loaded in (as in on server start or reload) so I think its a good compromise.

Server is back online so I will follow up with my script soon


That sounds great. You could also refresh the list periodically in the background using a timer (would have to be CleanUp’ed) or just every N requests (if at least some amount of time has passed since last update). Something like that. But even calling the API once after the config is loaded is probably fine for most use cases. :slight_smile:


below is my fail2ban action script (named cloudflare-ban.conf) that uses the cloudflare IP passed thru from the jail config which I will also post. You could use an API token instead of global API key instead if you wanted to, but for me the global key is just easier and im not concerned ( for the difference).

# Option:  actionstart
# Notes.:  command executed once at the start of Fail2Ban.
# Values:  CMD
actionstart =

# Option:  actionstop
# Notes.:  command executed once at the end of Fail2Ban
# Values:  CMD
actionstop =

# Option:  actioncheck
# Notes.:  command executed once before each actionban command
# Values:  CMD
actioncheck =

# Option:  actionban
# Notes.:  command executed when banning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:      IP address
#            number of failures
#            unix timestamp of the ban time
# Values:  CMD

actionban = curl -s -o /dev/null -X POST -H 'X-Auth-Email: <cfemail>' -H 'X-Auth-Key: <cftoken>' \
        -H 'Content-Type: application/json' -d '{ "mode": "block", "configuration": { "target": "ip", "value": "<ip>" } }' \

# Option:  actionunban
# Notes.:  command executed when unbanning an IP. Take care that the
#          command is executed with Fail2Ban user rights.
# Tags:      IP address
#            number of failures
#            unix timestamp of the ban time
# Values:  CMD

actionunban = curl -s -o /dev/null -X DELETE -H 'X-Auth-Email: <cfemail>' -H 'X-Auth-Key: <cftoken>' \
$(curl -s -X GET -H 'X-Auth-Email: <cfemail>' -H 'X-Auth-Key: <cftoken>' \
          '<ip>&page=1&per_page=1' | tr -d '\n' | cut -d'"' -f6)


cfemail = YOUR_EMAIL


below is a filter script (named i have to check the logged IP vs my current IP (not lucky enough to have a static IP currently)…

command -v dig >/dev/null 2>&1 && {
myIP=$(dig +short
} || {
myIP=$(curl --silent
} || {
exit 1
if [ “$ip” = “$myIP” ]; then
exit 0
exit 1

lastly, this is the jail configuration file that ties both the above together and works well for me:

enabled = true
port = http,https
filter = caddy-auth
action = iptables-multiport
logpath = /var/log/access.log
ignorecommand = /data/filter.d/ <ip>
bantime = 86400
maxretry = 5

hopefully this is helpful !!! let me know if you are still having issues.


I appreciate all of the information.

I’m stumbling before I can even get to your instructions, unfortunately.

I can’t for the life of me figure out where a “realip” block goes in a Caddyfile. I tried a number of positions and got either directive 'realip' is not ordered, so it cannot be used here or /etc/caddy/Caddyfile:5: unrecognized global option: realip (the latter makes it clear it’s not a global option!).

Edit: and once it’s in the right location, what is the config for logging in Caddy? Just something like:

log {
    output file /data/logs/access.log {
      roll_size 100MiB
      roll_keep 10
      roll_keep_for 72h
    format single_field common_log
    level INFO


Directives from plugins must be inside a route block so that they inherit some order. Only directives from standard modules have a default order defined:

1 Like

Man…just to get something as basic as fail2ban working with Caddy this is getting more complex than nginx or apache…

You’re talking about free / OSS software that is less than a year old, and comparing it to programs that are 15+ years old. Give the community time to integrate it with other programs, or contribute the solution yourself to help others. This is what it’s like to live on the edge!

1 Like

I know. I see things like GitHub - hslatman/caddy-crowdsec-bouncer: A (WIP) Caddy module that blocks malicious traffic based on decisions made by CrowdSec. are being worked on as well. I’m not frustrated with Caddy. I love it overall, just feels like the more I want to do with it, the more roadblocks I hit!

I’m starting to wonder if just going all-in on GitHub - greenpau/caddy-auth-portal: Authentication Plugin for Caddy v2 implementing Form-Based, Basic, Local, LDAP, OpenID Connect, OAuth 2.0 (Github, Google, Facebook, Okta, etc.), SAML Authentication wouldn’t be an easier overall solution to this.

Edit: I’m getting there, with this, though, now that I know it needs to be in a route.

@xnaas i use caddy-auth-portal as well. it doesnt have any functionality to mimic what fail2ban does although it is very secure.

the caddy community is always super helpful so keep chugging! follow up if you get it working or have any more issues.

for the record, regarding logs… i use this module

which is really just a fork of Mohammed90’s “format-encoder” except with the time formatting done in a way that is accepted by fail2bans regex syntax. so in my Caddyfile I use the following to generate my logs:

        log {
                format formatted
                output file /authdb/access.log {
                        roll_size 50MiB
                        roll_keep 5
                        roll_keep_for 48h

Alternatively, you can use the order global option at the top of the Caddyfile to say where in the default ordering (without having to define an explicit route) the new directive appears: Global options (Caddyfile) — Caddy Documentation

1 Like

Edit 2: Figured it out: order realip before route

Is there an example of how to format order in Caddyfile? If I just do:

map root header request_body redir rewrite uri try_files basicauth request_header encode templates handle handle_path realip route push respond metrics reverse_proxy php_fastcgi file_server acme_server

I get:

parsing caddyfile tokens for 'order': /etc/caddy/Caddyfile:5 - Error during parsing: unknown positional 'root'

Edit: This is in the global options section, of course.

@Austin_Kirsch: thank you again for all of the resources and help so far.

I think the only missing piece is what filter you use for caddy-auth. Did you just copy nginx-http-auth.conf or some such?

Edit: And just as a random note, I’m not really seeing any difference in logs between format formatted and format single_field common_log. Not sure if I’m missing anything or not.

Hey @xnaas here’s what’s working for me

failregex = ^<HOST>.+\s(401|403)
ignoreregex =
1 Like

Well…halfway there, I think? Here’s an example of a line when I try and login to a basicauth-protected subdomain from my mobile phone: - - [12/Jan/2021:03:48:39 +0000] "GET / HTTP/1.1" 401 0 is definitely a Cloudflare IP. I don’t know why realip is failing or what logs would be useful here.

Almost all other logging seems to be realip. But all 401s show Cloudflare IPs.

Additionally: is my log format correct? I have it set to format formatted, but it looks identical to format single_field common_log, so I’m not sure if it’s formatting as desired?

I can definitely sit here and do failed login attempts over and over and over without a ban happening, anyway. No logging from fail2ban, so it must be fine with whatever’s happening. :stuck_out_tongue:

Is the 401 because you’re using basicauth? In that case, it’s because basicauth or ordered before route.

Maybe it would be better to set realip to be ordered first or something like that.

1 Like