mholt / caddy-ratelimit

HTTP rate limiting module for Caddy 2
Apache License 2.0
255 stars 17 forks source link

multi-layer rate-limit configuration help needed #68

Closed teodorescuserban closed 1 month ago

teodorescuserban commented 1 month ago

Hello,

I am testing this amazing plugin, hoping i can migrate to caddy the ratelimiting part that is still in nginx.

After some trial and errors, I have a configuration that is working as expected. I would only need another feature and that is to be able to add IP ranges / classes to the bad_ips and our_ips map; unfortunately I was not able to do it.

How do you think I should approach this?

My current test config is below. Please note that I would rather keep the rate-limit block simple and obvious; in the real production environment I have quite a few dozens IP/subnets on both of those, as well as multiple domains to do the rate limit on; I will use snippets and templates, but I think it would be best to have the rate-limit matcher clear or enything else other than simple expressions (I know I can use client_ip matcher in the rate-limit, but can I use it in the map???).

The regex works but it is awful. 😄

Thank you in advance!

:80 {

    map {http.request.remote.host} {is_bad_ip} {
        192.168.65.19 0
        192.168.65.15 1
        default 0
    }

    map {http.request.header.Referer} {is_our_app} {
        default 0
        somereferer 1
    }

    map {http.request.remote.host} {is_our_ip} {
        default 0
        10.0.0.1 1
        192.168.65.13 1
    }

    rate_limit {
        log_key
        zone bad_ip {
            match expression `{is_bad_ip} == "1"`
            key {remote_host}
            events 1
            window 5s
        }
        zone our_ip {
            match expression `{is_our_ip} == "1"`
            key {remote_host}
            events 4
            window 1s
        }
        zone our_app {
            match expression `{is_our_app} == "1"`
            key {remote_host}
            events 5
            window 1s
        }
        zone unknown_source {
            match expression `{is_bad_ip} == "0" && {is_our_ip} == "0" && {is_our_app} == "0"`
            key {remote_host}
            events 1
            window 3s
        }
    }

    respond "You managed to get in!"
}
francislavoie commented 1 month ago

You could use https://github.com/tuzzmaniandevil/caddy-dynamic-clientip which allows you to have a dynamic list of IPs loaded into the matcher (detached from the config lifecycle, managed by the module), so you could have an IPSource module that you write that can read the list from a file and cache it for a few seconds.

teodorescuserban commented 1 month ago

Thank you for the quick reply, @francislavoie!

Ok, this sounds complicated, although I might do it. I was actually wondering if there is a way to have and / or operation between matchers to obtain a new matcher.

For my case, I could easily use

(bad_ips) {
    client_ip 192.168.65.0/24
}

and

(our_ips) {
        client_ip 10.0.0.0/8
}

for

                 zone bad_ip {
            match {
                import bad_ips
            }
            key {remote_host}
            events 1
            window 5s
        }   

and

        zone our_ip {
            match {
                import our_ips
            }
            key {remote_host}
            events 4
            window 1s
        }

but the real issue is how do i build the matcher for zone unknown_source ???

I would need to compose somehow the bad_ips and our_ips matchers to exclude both ranges from the unknown_source zone.

francislavoie commented 1 month ago

Explicit && and || is only possible via expression. You can use client_ip() inside a CEL expression. But you won't be able to import into the middle of an expression or w/e.

teodorescuserban commented 1 month ago

and I can't use named matchers inside an expression, correct?

teodorescuserban commented 1 month ago

Then back to the map idea. Can I use named matchers in themap? or maybe i can useclient_ip('10.0.0.0/8')` in the map? - I guess no.

francislavoie commented 1 month ago

No and no.

Really, you're best off writing your own plugin to manage this I think.

mholt commented 1 month ago

FWIW, plugins can "wrap" other plugins, so you just invoke their logic, and use their config structure, with anything extra that you want to add :)

You miiiight have more control over the matchers in JSON configuration though than you do in Caddyfile.

teodorescuserban commented 1 month ago

FWIW, plugins can "wrap" other plugins, so you just invoke their logic, and use their config structure, with anything extra that you want to add :)

Oh, that sounds really nice since my plan was to shamelessly copy paste from the http.handlers.map, remove all regex magic and add a simple net.ParseIP and net.ParseCIDR.

Do you have an example plugin that wraps other plugin to point to? It's a bit unclear for me at this point how to wrap map so I wont need any pasta work.

You miiiight have more control over the matchers in JSON configuration though than you do in Caddyfile.

teodorescuserban commented 1 month ago

As I said. Shameless pasta job 😁 https://github.com/teodorescuserban/caddy-ip-map

As always, @francislavoie and @mholt to the rescue. Thank you guys! ❤️

Closing this one.

mholt commented 1 month ago

Cool, glad you got something working.

In the future, you could literally embed the maphandler.Handler into your own struct, and as long as you also Provision() it during your Provision(), it you can then call its ServeHTTP() and benefit from reusing its code.

teodorescuserban commented 1 month ago

Thank you for suggestions!

The testing config looks like this and everything is working great:

{
    log {
        output file /logs/main.log
        format json {
            time_format iso8601
        }
    }
    local_certs
}

test.rate.local {

    # add here any naughty IP.
    ipmap {http.request.remote.host} {is_bad_ip} {
        default 0
        172.16.23.128/25 1
    }

    # add here any referrer that would need a higher rate limit than the rest.
    map {http.request.header.Referer} {is_our_app} {
        default 0
        ~^https://(myhost|trusted\.app)\.local 1
    }

    # ipmap {http.request.remote.host} {is_our_ip} {
    ipmap {remote_ip} {is_our_ip} {
        default 0
        127.0.0.1 1
        10.0.0.0/8 1
        192.168.22.59 1
    }

    rate_limit {
        log_key
        zone bad_ip {
            match expression `{is_bad_ip} == "1"`
            key {remote_host}
            events 1
            window 5s
        }
        zone our_ip {
            match expression `{is_our_ip} == "1"`
            key {remote_host}
            events 4
            window 1s
        }
        zone our_app {
            match expression `{is_our_app} == "1"`
            key {remote_host}
            events 5
            window 1s
        }
        zone unknown_source {
            match expression `{is_bad_ip} == "0" && {is_our_ip} == "0" && {is_our_app} == "0"`
            key {remote_host}
            events 1
            window 3s
        }
    }

    respond "{remote_ip} - {http.request.header.Referer}" 200
}