Can't Message Users Here, Reaching Out Via Thread

With order realip first, the 401 is still a Cloudflare IP, but the 200 afterwards with the username is the real IP.

162.158.187.54 - - [12/Jan/2021:15:29:57 +0000] "GET / HTTP/1.1" 401 0
162.202.x.x - username [12/Jan/2021:15:29:58 +0000] "GET / HTTP/1.1" 200 16874

Edit, I have this snippet in my Caddyfile, and there’s an import main on every domain/subdomain:

(main) {
  tls {
    on_demand
    dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    client_auth {
      mode require_and_verify
      trusted_ca_cert_file /data/origin-pull-ca.pem
    }
  }
  header {
    Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    Content-Security-Policy "upgrade-insecure-requests"
    X-Frame-Options "SAMEORIGIN"
    X-XSS-Protection "1; mode=block"
    Referrer-Policy "strict-origin-when-cross-origin"
  }
  log {
    output file /data/logs/access.log {
      roll_size 100MiB
      roll_keep 10
      roll_keep_for 72h
    }
    format formatted
    level INFO
  }
  realip {
    header "X-Forwarded-For"
    from cloudflare
    maxhops 5
  }
}

@xnaas

i may be wrong but i think what @francislavoie was saying is that it could be due to Caddyfile default ordering processes basicauth before route…

what you should do in this case is have the first lines of your Caddyfile to say something like:

{
  order realip before basicauth
}

try this and follow up! i use caddy-auth-portal which obviously is processed after all internal Caddy configs so thats why i never ran into this issue.

edit:

just saw that you’ve done this already. sorry. in that case, @matt or @francislavoie may have to step back in, im drawing a blank

Yeah - I don’t know where the 401 is coming from so I couldn’t say.

Is the 401 from client_auth? If so I think it might be impossible to get the IP for those because that happens before Caddy executes its handlers, unless you used a listener_wrapper plugin instead (which AFAIK doesn’t exist).

I’m only using basicauth. The three modules I have installed with Caddy are:

The 401 immediately appears when accessing a subdomain protected with basicauth. Relevant Caddyfile sections:

(main) {
  tls {
    on_demand
    dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    client_auth {
      mode require_and_verify
      trusted_ca_cert_file /data/origin-pull-ca.pem
    }
  }
  header {
    Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    Content-Security-Policy "upgrade-insecure-requests"
    X-Frame-Options "SAMEORIGIN"
    X-XSS-Protection "1; mode=block"
    Referrer-Policy "strict-origin-when-cross-origin"
  }
  log {
    output file /data/logs/access.log {
      roll_size 100MiB
      roll_keep 10
      roll_keep_for 72h
    }
    format formatted
    level INFO
  }
  realip {
    header "X-Forwarded-For"
    from cloudflare
    maxhops 5
  }
}
(asak) {
  basicauth {
    {$ASAK}
  }
}
https://secure-example.xnaas.info {
  import main
  import asak
  root * /www/download
  redir /favicon.ico https://xnaas.info/favicon.ico
  file_server browse
}

The second you access secure-example.xnaas.info, the 401 appears in logs. If you put failed username/passwords in over and over, it’ll continue to be 401s with Cloudflare IPs. The real IP is only revealed upon successful login (HTTP 200).

Is this just another limitation/issue with basicauth, then? Wouldn’t be the first time it’s caused so much trouble. :stuck_out_tongue:

Edit: At this point it seems to mostly work for anything other than basicauth.

Okay, I see what’s going wrong now.

So you see that above this section, if there’s no error, Caddy returns. But if there’s an error – which is the case with basicauth because it returns an actual caddyhttp.Error(http.StatusUnauthorized, ...) – it resets the RemoteAddr on the request before it gets logged.

This functionality was implemented in this PR:

https://github.com/caddyserver/caddy/pull/3781

@matt I think we may want to rethink how this is handled, because there is a legitimate usecase for wanting the RemoteAddr to be permanently modified here.

Or… now that I think of it, maybe the realip plugin should also mutate origReq := r.Context().Value(OriginalRequestCtxKey).(http.Request) so that Caddy doesn’t need to change? But that does feel like too funky of a fix (would introduce a weird hidden behaviour).

Edit: Oh aaaaaactually, what if you add realip to handle_errors?

handle_errors {
	realip
}

:thinking:

I know common_log is deprecated, but I think that’s the root of the issue here.

Here’s a trimmed example request:

{"level":"error","ts":1610663995.7737784,"logger":"http.log.access.log1","msg":"handled request","request":"common_log":"162.158.63.57 - - [14/Jan/2021:22:39:55 +0000] \"GET / HTTP/1.1\" 401 0","duration":0.000149888,"size":0,"status":401,"resp_headers":{"X-Xss-Protection":["1; mode=block"],"Content-Security-Policy":["upgrade-insecure-requests"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Strict-Transport-Security":["max-age=63072000; includeSubDomains; preload"],"X-Frame-Options":["SAMEORIGIN"],"Www-Authenticate":["Basic realm=\"restricted\""],"Server":["Caddy"]}}

common_log (which I think realip reads off of) uses the remote_addr field instead of X-Forwarded-For.

Edit: Actually, further log reading shows it to be wildly inconsistent…sometimes the common_log (or formatted) has the X-Forwarded-For address and sometimes it has remote_addr.

No effect.

What realip does is actually modify r.RemoteAddr to have the value of X-Forwarded-For. But Caddy is resetting that change if an error occurred.

realip knows nothing of the logs at all. That’s not its job.

If you just used it as I wrote it, it probably wouldn’t work - you need to specify the same subdirectives to it as you did before, I just wrote that as a concept. I’d recommend pulling your realip into a snippet that you can plug into both your site block and your handle_errors block.

Yes, I tried this:

https://secure-example.xnaas.info {
  import main
  import asak
  handle_errors {
    realip {
      header "X-Forwarded-For"
      from cloudflare
      maxhops 5
    }
  }
  root * /www/download
  redir /favicon.ico https://xnaas.info/favicon.ico
  file_server browse
}

No effect. Still getting Cloudflare IPs on basicauth attempts.

And before you ask, yes, I am in fact reloading my Caddyfile. :wink:

Edit: I lied, the effect is WORSE, actually:

162.158.62.236 - - [14/Jan/2021:23:18:06 +0000] "GET / HTTP/1.1" 0 0

The HTTP status code vanishes entirely. :scream:

Maybe you need to add this to the end of the handle_errors block to preserve the response code :thinking:

respond "{http.error.status_code} {http.error.status_text}" {http.error.status_code}

But yeah idk what to say past this point, but there’s clearly a problem with the order of operations here that causes errors being logged to have their RemoteAddr reset.

That does indeed preserve the 401 status code, but we’re still SOL on RemoteAddr. Guess I’ll wait for feedback from @matt on that.

Just catching up… I will try to look into it soon. In the meantime, could you also reach out to the developer of realip and see if it’s intended to work in error routes too? My understanding is that it just sets RemoteAddr to the value of X-Forwarded-For. Since headers aren’t changed when invoking the error routes, this should still work in the handle_errors block.

That would be @Austin_Kirsch I believe.

I’ve opened realip doesn't set the RemoteAddr correctly within basicauth · Issue #4 · kirsch33/realip · GitHub

Just to add searchable information here for others in the future, I ended up getting frustrated and dedicating the time to just parsing Caddy’s JSON logging.

NOTE: I use Authenticated Origin Pulls so my site cannot be accessed via my IP and only through Cloudflare. I also have a static IP.


In my /etc/fail2ban/jail.local file, I have this:

ignoreip = MY_STATIC_IP
[caddy-auth]
enabled = true
port = http,https
filter = caddy-auth
action = cloudflare-ban
logpath = /path/to/logfile/access.log
bantime = 86400
findtime = 15m
maxretry = 5

My /etc/fail2ban/filter.d/caddy-auth.conf:

[Definition]
failregex = ^.*\"Cf-Connecting-Ip\":\[\"<HOST>\"\].*\"status\"\:(401|403).*$
ignoreregex =

My /etc/fail2ban/action.d/cloudflare-ban.conf:

[Definition]
# 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>" } }' \
        https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules

# 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>' \
          https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules/$(curl -s -X GET -H 'X-Auth-Email: <cfemail>' -H 'X-Auth-Key: <cftoken>' \
          'https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules?mode=block&configuration_target=ip&configuration_value=<ip>&page=1&per_page=1' | tr -d '\n' | cut -d'"' -f6)

[Init]

cfemail = YOUR_CF_EMAIL

cftoken = YOUR_GLOBAL_TOKEN

I think there’s room for improvement somewhere, probably, but I’ve tested that this works just fine for now.


In my Caddyfile, I have:

(main) {
  tls {
    on_demand
    dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    client_auth {
      mode require_and_verify
      trusted_ca_cert_file /data/origin-pull-ca.pem
    }
  }
  header {
    Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    Content-Security-Policy "upgrade-insecure-requests"
    X-Frame-Options "SAMEORIGIN"
    X-Content-Type-Options "nosniff"
    X-XSS-Protection "1; mode=block"
    Referrer-Policy "strict-origin-when-cross-origin"
  }
  log {
    output file /data/logs/access.log {
      roll_size 50MiB
      roll_keep 5
      roll_keep_for 72h
    }
    format json
    level ERROR
  }
}

For every https://example.com/ or https://sub.example.com/ I have defined in my Caddyfile, I have an import main line first.

The log and tls sections are what are most relevant to the above fail2ban situation being functional.

FYI, if you aren’t aware of jq, I would recommend making use of that for extracting the relevant bits from your logs.

https://stedolan.github.io/jq/

1 Like

I’ve heard of jq before (and even tried to use it with huginn once upon a time), but I think that’s just even more added complexity to this than I want haha

Edit: This has already become incredibly non-portable as it is… :upside_down_face:

@xnaas

My module simply overwrites RemoteAddr before passing the request to caddyhttp.ServeHTTP

What is happening in this case is that basicauth requests are passed along as usual except once there, the overwritten RemoteAdddr is re-overwritten with the very original IP since its flagged with an error as shown here:

So without changing everything about how its working right now, the easiest solution might be for me to change BOTH request RemoteAddr values before passing to ServeHTTP. Something similar to:

origReq := r.Context().Value(OriginalRequestCtxKey).(http.Request)
    req.RemoteAddr = net.JoinHostPort(parts[len(parts)-1], port)
    origReq.RemoteAddr = net.JoinHostPort(parts[len(parts)-1], port)
        for i := len(parts) - 1; i >= 0; i-- {
            req.RemoteAddr = net.JoinHostPort(parts[i], port)
            origReq.RemoteAddr = net.JoinHostPort(parts[i], port)

@matt @francislavoie any thoughts on proceeding down this path?

That is what I suggested up here:

But it’s up to you. I would suggest that you clearly note that your plugin would do this, so that people are aware.