I’m trying to separate out public and private services reverse-proxied behind Caddy. Public services should be accessible over the Internet, while private services should only be accessible via a VPN connection.
In the example configuration below, assume the following:
I’ve set public.example.com’s DNS entry to the public IP of the server (say 123.123.123.123)
I’ve set private.example.com’s DNS entry to the private VPN IP of the server (say 10.0.20.124)
All wildcard subdomains map to the subdomain one level above with CNAME records
All DNS entries have been published publicly
I am guessing that service C in the example configuration cannot be accessed through the Internet? Is there a way a malicious user could trick Caddy into exposing/serving service C over the public portion of the server? Say for example, the client makes a connection to public.example.com but somehow tricks Caddy into believing the requested host name is servicec.private.example.com?
Malicious users can send the Host: serviceb.private.example.com header to any IP address they choose, so it’s still possible to access the private host names from the public IP address.
One way to do that with an unaltered browser is to simply map your private host names to your server’s public IP using hosts.txt or their local recursive DNS server. The first can be done with Notepad, so this doesn’t even require deep technical knowledge, so this just requires (a) knowing about hosts.txt (i.e. the ability to Google) and (b) knowing which host names your server is configured to respond to.
If the public IP is bound directly to the server (not via NAT mapping or something), use the bind directive to restrict your private host names to respond only on your private IP.
In other words, putting bind 10.0.20.124 in your *.private.example.com block should ensure they are not accessible unless the client connects to that IP address, but that only works if the public IP isn’t NAT-mapped to the private IP (because in that case all connections look like they come in on the private IP).
If you do have the public IP NAT-mapped to the private IP, you can just add another private-range IP to your server. That’s probably easiest with IPv6, which is required to support multiple IPs per interface. So add an ULA IP (any random IP in fd00::/8 will do, as long as it’s unique in your network) or use the link-local IPv6 (the one that starts with fe80:).
I see you’ve marked my previous reply as a solution, but since I spent some time figuring this out I’ll post it anyway.
You can listen to different ports in the container, and have Docker bind to the interfaces for you, forwarding to the correct port depending on the interface.
A simple HTTP-only example follows. Replace $PUBLIC_IP and $PRIVATE_IP with the respective values (or define them in a .env file).
# Public server listens on port 80
http://public:80 {
reverse_proxy http://whoami-public
}
# Private server listens on port 81
http://private:81 {
reverse_proxy http://whoami-private
}
@fvbommel I guess that would work if I flipped it around and made the public server listen on port 81, then had the router forward port 80 requests to 81. In my case I just wanted the simplicity of having Caddy listen on one port (and also my server OS, unRAID, doesn’t support Docker Compose to the best of my knowledge).
Also, I ended up using two Caddy instances, because unless Caddy is running in the host network mode it thinks all clients connect from the Docker internal IP. So I do the IP filtering first on the outer Caddy instance before passing it through to the internal Caddy server.