"Host not allowed" when calling the API remotely

1. My Caddy version (caddy version):

~ # ./caddy version                                                                                                                                                            
v2.0.0-rc.3 h1:z2H/QnaRscip6aZJxwTbghu3zhC88Vo8l/K57WUce4Q=

2. How I run Caddy:

~ # ./caddy run                                                                                                                                                                
2020/04/23 08:19:05.502 INFO    admin   admin endpoint started  {"address": "tcp/localhost:2019", "enforce_origin": false, "origins": ["localhost:2019", "[::1]:2019", "127.0.0.1:2019"]}
2020/04/23 08:19:05.502 INFO    serving initial configuration

a. System environment:

Ubuntu 18.04

d. My complete Caddyfile or JSON config:

None (see below)

3. The problem I’m having:

When trying to access a modified API endpoint (see below for the fill scenario) I get

>curl http://192.168.10.2:2020/config/
{"error":"host not allowed: 192.168.10.2:2020"}

5. What I already tried:

I started Caddy without any configuration caddy run and then pushed a new configuration via the API so that the admin endpoint is available outside of localhost:

~ # curl localhost:2019/load \                                                                                                                                                 root@srv
        -X POST \
    -H "Content-Type: application/json" \
    -d @- << EOF
{
"admin": {
    "listen": ":2020"
    }
    }
EOF

This modifies the endpoint on the server, as seen in its logs:

2020/04/23 08:19:20.828 INFO    admin.api       received request        {"method": "POST", "host": "localhost:2019", "uri": "/load", "remote_addr": "127.0.0.1:58382", "headers": {"Accept":["*/*"],"Content-Length":["42"],"Content-Type":["application/json"],"User-Agent":["curl/7.58.0"]}}
2020/04/23 08:19:20.832 INFO    admin   admin endpoint started  {"address": "tcp/:2020", "enforce_origin": false, "origins": [":2020"]}
2020/04/23 08:19:20.832 INFO    autosaved config        {"file": "/root/.config/caddy/autosave.json"}
2020/04/23 08:19:20.832 INFO    admin.api       load complete
2020/04/23 08:19:21.333 INFO    admin   stopped previous server

I now tried to connect to that endpoint from anywhere (including localhost) and I get the follwing error message:

>curl http://192.168.10.2:2020/config/
{"error":"host not allowed: 192.168.10.2:2020"}

On the server side, the logs agree:

2020/04/23 08:19:26.594 INFO    admin.api       received request        {"method": "GET", "host": "localhost:2020", "uri": "/config/", "remote_addr": "127.0.0.1:36698", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.58.0"]}}
2020/04/23 08:19:26.595 ERROR   admin.api       request error   {"error": "host not allowed: localhost:2020", "status_code": 403}
2020/04/23 08:37:05.888 INFO    admin.api       received request        {"method": "GET", "host": "192.168.10.2:2020", "uri": "/config/", "remote_addr": "192.168.10.72:54372", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.55.1"]}}
2020/04/23 08:37:05.888 ERROR   admin.api       request error   {"error": "host not allowed: 192.168.10.2:2020", "status_code": 403}

Where should I define which IP addresses (or ranges) are allowed to access the API?

I also tried to put an actual IP address for the interface (192.168.10.2:2020 instead of :2020) but the problem is the same.

EDIT: it would also seem that the API access should be open: v2: Access controls for admin API · Issue #2850 · caddyserver/caddy · GitHub

I had a look at the relevant piece of code

// checkHost returns a handler that wraps next such that
// it will only be called if the request's Host header matches
// a trustworthy/expected value. This helps to mitigate DNS
// rebinding attacks.
func (h adminHandler) checkHost(r *http.Request) error {
	var allowed bool
	for _, allowedHost := range h.allowedOrigins {
		if r.Host == allowedHost {
			allowed = true
			break
		}
	}
	if !allowed {
		return APIError{
			Code: http.StatusForbidden,
			Err:  fmt.Errorf("host not allowed: %s", r.Host),
		}
	}
	return nil
}

From that I derive that the host IP (the host caddy runs on) must be in origins. I tried

{
    "admin": {
        "listen": ":2020",
        "origins": ["192.168.10.2:2020"],
        "enforce_origin": false
    }
}

and it works!

Since the docs say that origin is not used when enforce_origin is true, I guess that this should be changed in the documentation (if my analysis is correct).

One drawback of this solution is that I have to explicitly set the IP I will bind to (in origin, but not in listen).

@WoJ RC3 doesn’t have the latest fixes from about a week ago: admin: Disable host checking if wildcard interface is specified · caddyserver/caddy@f5ccb90 · GitHub - the website also doesn’t have the latest docs, which will be updated when 2.0 is released.

Thanks Matt, this is good news!