Caddy directive order

1. The problem I’m having:

Caddy directive order is not working as documented

2. Error messages and/or full log output:

No errors

3. Caddy version:

v2.10.0

4. How I installed and ran Caddy:

a. System environment:

Archlinux

b. Command:

N/A

c. Service/unit/compose file:

Default service file.

d. My complete Caddy config:

See below

5. Links to relevant resources:


I have been using caddy for ~5 years now. Recently, I realised I have
misconfigured such a way that my setup is insecure.

I am using caddy as the reverse proxy for multiple self-hosted sites in my
home server. Some of the sites are public (E.g. blog, git forge) and some of them
are private. (E.g. Jellyfin, Photoprism).

Following this wiki post, I created the below snippet

(private_domain) {
    @blacklist {
        not remote_ip 192.168.0.0/16 100.64.0.0/10 127.0.0.1
    }
    respond @blacklist "Forbidden" 403 {
        close
    }
}

In all the private sites, I included import private_domain to only allow from
private network. Unfortunately this does not work as expected.


Example

Simple example to reproduce the problem

http://hello.local:8100 {
     @blacklist {
         not remote_ip 127.0.0.1
     }

    respond @blacklist "Forbidden" 403

    respond "hello world"
}

Seems to work fine so far. When accessing from LAN ip, it is ‘Forbidden’ and
works as expected in localhost

Local

❯ curl --resolve hello.local:8100:127.0.0.1 http://hello.local:8100
hello world

Remote

❯ curl --resolve hello.local:8100:192.168.0.2 http://hello.local:8100
Forbidden

Both works fine as expected. Then added a new path for some requests.

http://hello.local:8100 {
     @blacklist {
         not remote_ip 127.0.0.1
     }

    respond @blacklist "Forbidden" 403

    handle /api.json {
        respond `{"success": "ok"}` 200
    }

    respond "hello world"
}

Now the existing path still works as expected. But the new path /api.json
escapes the blacklist and is served 200.

Local

❯ curl --resolve hello.local:8100:127.0.0.1 http://hello.local:8100
hello world

❯ curl --resolve hello.local:8100:192.168.0.2 http://hello.local:8100/api.json
{"success": "ok"}

Remote

❯ curl --resolve hello.local:8100:192.168.0.2 http://hello.local:8100
Forbidden

❯ curl --resolve hello.local:8100:192.168.0.2 http://hello.local:8100/api.json
{"success": "ok"}

Because the blacklist was previously working fine, I didn’t bother to check it
again. :frowning:

I tried to wrap the ‘Forbidden’ response inside a handle

http://hello.local:8100 {
     @blacklist {
         not remote_ip 127.0.0.1
     }

     handle @blacklist {
        respond "Forbidden" 403
     }

     handle /api.json {
        respond `{"success": "ok"}` 200
     }

     respond "hello world"
}
❯ curl --resolve hello.local:8100:192.168.0.2 http://hello.local:8100
Forbidden

❯ curl --resolve hello.local:8100:192.168.0.2 http://hello.local:8100/api.json
Forbidden

This works, but it is not supposed to work according to Sorting Algorithm.

Same-named directives are sorted according to their matchers.

Here, handle @blacklist and handle /api.json.

The highest priority is a directive with a single path matcher.

A directive with any other matcher is sorted next, in the order it appears in the Caddyfile.
This includes path matchers with multiple values, and named matchers.

Single path matcher /api.json, should have the higher priority over named
matcher @blacklist. So the blacklist should not have worked. But somehow this
works!

Questions

  1. In my original private_domain snippet, If I just wrap the respond with a
    handle, will it just work fine?
  2. Is there an easy way to debug all this and see the order caddy executes the
    config? Suggestion: caddy fmt --reorder-directives can re-order the
    caddyfile according to the algorithm, so user gets WYSIWYG behaviour.