Using an ACME DNS challenge would be the simplest option if it wasn’t such a pain to integrate with most DNS services; but even HTTP challenges don’t actually need to expose the same server that actually runs the service, just one that serves /.well-known/acme-challenge/* during the validation process. (For example, this could be the same server via access control rules that check what interface the request came in on, or a completely different server with split-horizon DNS and/or routing, or a special service running on port 80 that’s only used for challenges.)
(I was thinking about this a lot recently because I had a service that wanted to do HTTP challenges but I didn’t want to put the whole thing on the Internet. In the end my solution was to assign an IPv6 range which is routed by VPN internally but to a proxy server for public requests: https://search.feep.dev/blog/post/2025-03-18-private-acme)