56 comments

[ 3.8 ms ] story [ 36.6 ms ] thread
I've documented how to securely set up TLS certificates for internal services without creating TLS issues for http clients downstream. All thanks to split-horizon DNS, WAF and ACME protocol. All for free!
I don't agree that tunneling everything through some external facing proxy is "TLS certificates for internal services done right".
I use the acme dns-1 challenge on my public domain. That gives you certificates you can use as you see fit, without needing to expose anything else to the public internet.

I also use Tailscale so I configure my DNS to use my Tailscale IP addresses. If you don’t want to expose them on a public DNS server you can add them only to an internal DNS server.

this is all fine and good, if you are okay broadcasting your internal hostnames. I suppose it's a trade off some might make.
I use a registered domain with DNS validation and then CNAMEs that I resolve locally. Basically:

  1. Register a domain ("server.com") and put it on some public DNS that can do DNS validation with acme.sh.
  2. Use DNS validation to get a certificate on your domain from Let's Encrypt. You can just grab a wildcard one ("*.server.com").
  3. CNAME all of your services on a public DNS to an internal address ("email.server.com" → "server.internal", "plex.server.com" → "server.internal").
  4. Resolve your internal address on a local DNS server with an A record ("server.internal" → 192.168.0.123). This can often just be done on your router.
Since you use DNS validation, you just API keys for your public DNS service that acme.sh can use. No need to have any VPN network interfaces for getting your certificate. Your wildcard certificate also doesn't leak any details about your services.
I like this approach. Thank you.

Would it work if a user's device that is already connected to the VPN, but has custom DNS override to say 8.8.8.8 ? How can I allow my users to be able to use 8.8.8.8 DNS override and still work seamlessly?

Why not just map the domain to an internal IP and call it a day? Then the only way it can be accessed is through a VPN. Then use a wildcard so none of leaks into cert transparency logs
My preferred procedure is to use DNS-01 validation and have no publicly accessible "A" or "AAAA" record for internal services.

Or even a more extreme example: https://crt.sh/?id=27555237869 (sorry for any possible crt.sh downtime) - the domain name in question never existed in public or private DNS by itself. It is used only for a WPA3-Enterprise network, as the CN that WiFi clients expect to be present in the RADIUS server certificate, but never resolve. In the public DNS, only the "_acme-challenge" TXT record exists.

The relative proximity of the words "done right" and "split-horizon DNS" makes my insides hurt a little bit.

Use DNS validation to allow these internal services to pull ACME certs. There's so much less headache, long-term.

Split-horizon DNS (and the tedious make-work it can create when you start needing to mirror public-accessibly records in the private DNS) has always been something to aspire to move away from in my experience.

Another big problem with split-horizon is you can get clients that cache the dns result before connecting to the vpn, then after connecting, can't actually use the service, because it is using the public ip instead of the private one.
> because it is using the public ip instead of the private one.

For this edge case, the issue is not with DNS; it's that the router is not configured to allow internal clients to access the network through the public IP, vis-à-vis hairpin routing.

Yes - literally just pick the right Caddy (or similar) image with Let's Encrypt client included - and you can simply add a tag/label to your docker compose files for each of self hosted services to get a real SSL, that auto renewed ...etc.

With one of self hosted services being Adguard-Home can do both ADs blocking and internal DNS... The public DNS records for your "internal use only" domain remain empty.

... Could you please provide a solution? What should I do in my homelab?

Saying something isn't bad without pointing to right direction makes my insides hurt a little bit.

Personally, I hate split horizon DNS. I prefer the "BeyondCorp" model. I MUCH prefer putting an mTLS cert in my trusted devices over relying on VPNs in same devices. I've yet to see a "clever" DNS setup not cause annoyances.

Specifically grafana is nice to be able to see on the phone, and split horizon DNS and corp VPN is a hassle, to say the least, on phones.

I bet you can do it with HA-Proxy, but I use https://github.com/ThomasHabets/sni-router

Zero trust or bust. I actually cringed when I read "done right" applied to split-horizon DNS.
I am looking forward to finally using DNS-PERSIST-01 for validation. No more dynamic DNS updates, DNS credentials or forwarding necessary.
Sadly most tools still doesn't support it: https://github.com/cert-manager/cert-manager/issues/8373#iss...

And then the issue is protecting the private key of the issuer and monitoring certificates (it's a good idea to do that anyway).

One of the draft authors is already working on a cert-manager implementation. I'm optimistic it'll make it into cert-manager once the spec is ready and CAs actually start to implement it.
The real answer here is that configuring HTTPS clients to trust a self-signed cert (or signed by an internal CA) shouldn't be as difficult as it is. I find it extremely annoying that every programming language has it's own idea of where certificates should live instead of just checking the os trust store.
Is there a commonly used language other than Java that doesn’t just defer to the OS trusted CAs by default?
Possibly a separate concern, but I have some degree of confidence that the requirements and oversight of the CA/B forum (or whomever else determines which root certs go into bundles) are sufficiently strict, and issuers kept under sufficient scrutiny, that I tend to trust those more than I trust myself to secure my own root CA keys adequately. The ideal would be for people setting up their own PKI to ensure their root uses the Name Constraints extension, but the default “can sign anything for any host” I fear makes it easy for people to install their own self-pwn device, and probably left the private key lying around on a box exposed to the Internet.

* with some notable root certs that I have… questionable… trust and confidence are not simply controlled by certain state actors.

This is crazy. If you have a home network with a few internal services, or some sort of network where you don't control the endpoints, just use DNS validation. That's why it exists.

But on hosts you control, you should absolutely provision them with an identity and join the local CA. You're going to need it for a multitude of other reasons.

> TLS certificates for internal services* done right

* "internal services" = on a single server that is publicly routable

Hmm. I don't really care enough about leaking home network host names because they are all super generic names like 'router', 'laptop', 'tv', 'nas'. So I use my public zone on cloudflare. I just use internal ip addresses (eg: nas.example.com = 10.1.2.3) on the public zone and DNS01 challenge for let's encrypt. Anyone can resolve the ip for any of my hosts, but obviously you'd need to be on the wireguard vpn to hit them.

This means that I can always use public DNS servers like 1.1.1.1, 8.8.8.8, nextDNS etc

This is not "done right" by any stretch but it's extremely low effort to set up and has never once failed me, unlike countless complex meshy things.

At that point why not just use the .ts.net addresses Tailscale provides for free?
I get to use the same host names and IP addresses at home or on the VPN. I run a simple wireguard server on a Raspberry pi, and the whole setup has been rock solid.

I did set up tailscale, way back. After using it a few times to test, it failed me when I really needed it (I was out of the country and it failed - can't remember exactly what went wrong but it wwas 100% 'in my tailscale account'). I immediately dropped it and went back to OpenVPN (shit but reliable) before building my current setup.

I don't think this is any less right than using split horizon. IMHO, there is no "right" way to do it. Every approach has downsides and tradeoffs.
Subdomains with wildcard dns along with wildcard certificates solves that issue. You only expose a wildcard domain with no subdomains.
Don't do this. Public certs are for public services.
I wonder if the author realizes that getting public certificates results in them being recorded in CT logs.
Yep. Especially with non-wildcard certs this leaks all service names (privacy concern).
I don’t know much in this space, but I find myself wishing there was a dead simple self hosted CA solution and also that trust on first use (à la ssh) was A Thing for self-managed root certs in client implementations. TOFU is such an elegant, good-enough solution for these use cases. Fixed deployment is always still an option, but in this day and age it feels so much like we are unnecessarily still dealing with solved problems
I was under impression that I understand networking and DNS resolution. It was really hard to follow, the OP did worked hard, just not sure what exact problem was being solved with the proposed solution that isnt already been solved.
"Right" without use case can be wrong. And by use case I include scale. For a small team, few machines, some in-place infrastructure may worth it. Smaller than that may be overkill, bigger than that may not be enough, or end being cumbersome, insecure or not work for everyone.
A Github Action running acme.sh that pushes certs to S3 solves the split dns issue for hosts, which can cause all sorts of weirdness after a while. You can then grab a cert on a schedule and even make them wildcard if you want. Then you will get NXDOMAIN if you are not on the VPN so ideally no public traffic.
Split horizon DNS is not something I'm willing to do. I'll just rock out with .internal or .home.arpa, have step-ca and bind communicate to each other, either in step-issuer in kube or maybe even rfc2136 if i feel like a bit of the dns-01 strategy is in order. I slap the internal ca root certs everywhere, and keep my home infra out of the crt.sh logs.

I get it, I could just do *.mydomain.com and slap that wildcard cert everywhere, but it's still in the public logs..

When using letsencrypt for internal services it becomes very hard to distinguish badplayer.com LE certificate from the good players. You only ensure encryption. Not identity. Do not use letsencrypt internally for things that matter.
Or...

- Don't use split DNS. Don't use any special internal or dev domain. Leave it to your infrastructure to route/NAT those public IPs to your internal network.

- Don't use the HTTP-01 challenge. Use DNS-01.

- Don't run your own internal CA. Use Let's Encrypt. If you care about name leakage (CT Logs), use wildcard certs. Use a central reverse proxy/load balancer for termination.