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.
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
- In my original
private_domain
snippet, If I just wrap therespond
with a
handle
, will it just work fine? - 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.