HTTPS for homelab services, without the certificate warnings
You can have valid certificates on services nobody outside your network can reach, with no open ports and no warnings on any device. Here is the setup that does it.
Every homelab reaches the same awkward stage. You have eight services running, each on a different port, and every one of them is plain HTTP or throws a certificate warning. You click through the warnings for a while, then you get tired of it, then you go looking for a proper answer and find four contradictory ones.
There is a clean way to do this. It gives you real, publicly trusted certificates for services that are not reachable from the internet, with no open ports and no warnings on any device. It takes an afternoon to set up and then it runs itself.
Why internal HTTPS is awkward
A certificate is a statement by an authority that a particular name belongs to you. That works because the authority verifies the claim before signing, and the usual verification is "prove you control the web server at this public address."
Your homelab breaks both halves of that. The service is not reachable from the internet, so the authority cannot connect to it, and the name you want to use is often something like nas.home that nobody can own in the first place.
Hence the two bad habits that follow. Either you skip HTTPS entirely, or you generate a self signed certificate, which is a statement by you that a name belongs to you, and no browser has any reason to believe it.
The .local trap
Before anything else: do not use .local for a DNS zone.
.local is reserved for multicast DNS. When a device looks up nas.local, it is supposed to shout the question onto the local network and wait for the device itself to answer, not ask a DNS server. Some operating systems refuse to send .local queries to a resolver at all.
The result is a zone that works on some devices and not others, in ways that look random. Apple devices behave one way, Android another, Linux depends on the resolver configuration, and containers do their own thing. You can spend a long time debugging that before discovering it was never going to work consistently.
What to use instead:
- A subdomain of a real domain you own.
home.example.com, with services atnas.home.example.com. This is the recommended option and the only one that allows publicly trusted certificates. .internal, which is now formally reserved for private use. Safe from ever colliding with a real TLD, but no public authority will issue a certificate for it, so you are back to running your own.- Not
.home,.lanor anything you invented. These are unreserved and could be delegated as real top level domains, at which point your internal names start resolving to somebody else's servers.
Domains are cheap. Buying one purely for your homelab is a reasonable thing to do, and it unlocks everything below.
Four approaches, honestly compared
| Approach | Effort | The catch |
|---|---|---|
| Plain HTTP internally | None | Fine for a single user network with nothing sensitive. Breaks browser features that require a secure context, and means credentials cross your LAN in the clear, which matters more once guests and IoT devices share it. |
| Self signed per service | Low | A warning on every service on every device, forever. You train yourself to click through certificate warnings, which is the habit you least want. |
| Your own internal CA | Medium | Works properly, but every device must trust your root. That is a manual step on every laptop, phone, tablet and container, and iOS needs both a profile install and a separate switch to enable full trust. New device, new chore. |
| Real certificates via DNS validation | Medium, once | Requires a domain you own and a DNS provider with an API. In exchange, every device trusts it with no configuration at all. |
The last one is the answer for most people, and the reason it works is worth understanding.
The setup worth building
The key idea: certificate authorities offer a DNS based challenge as well as an HTTP one. Instead of proving you control a web server, you prove you control the domain by creating a specific TXT record. Nothing needs to be reachable from the internet for that to work.
So you can hold a valid, publicly trusted certificate for nas.home.example.com while that name resolves to 192.168.1.50 and is reachable only from your own network.
-
Own a domain and use a DNS provider with an API
Most ACME clients support the common providers directly. This is the only hard requirement.
-
Issue a wildcard certificate using DNS validation
Request
*.home.example.com. One certificate covers every service you will ever add, so adding a new one later involves no certificate work at all. Wildcards can only be issued through DNS validation, which is convenient because that is what you wanted anyway. -
Point internal DNS at private addresses
Your internal resolver answers
nas.home.example.comwith192.168.1.50. Public DNS either has no record at all or points at a tunnel for the few services you deliberately publish. This is split horizon DNS, and it is what lets a public name resolve to a private address inside your network. -
Put one reverse proxy in front of everything
Caddy, Traefik, nginx, whichever you prefer. It holds the wildcard certificate, terminates TLS once, and routes by hostname to each service's port. Now every service is
https://something.home.example.comwith no port numbers to remember, and there is exactly one place where certificates live. -
Automate renewal, and reload the proxy
Renewal writes a new file. The running process keeps the old certificate in memory until it is told otherwise. A surprising share of "renewal did not work" reports are really "nobody reloaded the proxy," so make the reload part of the renewal hook and then verify it from outside the process.
One detail that catches people: the certificate is valid for *.home.example.com, which covers nas.home.example.com but not home.example.com itself, and not a.b.home.example.com. Wildcards match exactly one label. Add the apex explicitly if you want it.
Check what your proxy is actually serving
TLS Certificate Inspector shows expiry, the SAN list and the issuer chain for any host on your network, so you can confirm the wildcard really covers the name you typed and that the intermediate is being sent. Pair it with NTP Time Check, because a drifting clock invalidates every certificate at once.
One privacy detail worth knowing
Every certificate issued by a public authority is published in Certificate Transparency logs, which are public and searchable. That is a good thing: it is how misissued certificates get caught.
It does mean that issuing individual certificates for nas.home.example.com, vaultwarden.home.example.com and proxmox.home.example.com publishes a list of your internal service names to anyone who cares to look. Nothing is exposed, nobody can reach them, but the inventory is public.
A wildcard avoids this neatly. *.home.example.com appears in the logs and reveals nothing about what runs behind it. It is a small thing, and it is a genuine reason to prefer the wildcard beyond the convenience.
Verifying it properly
Four checks, and each one catches a different common mistake.
Does the name resolve to the right address? A DNS lookup from a device on your network should return the private address. Run it again against 1.1.1.1: a public resolver should return nothing, or your tunnel, but not your internal address. If a public resolver is handing out 192.168.1.50, your split horizon is leaking.
Is the service actually listening? A TCP connect to port 443 on the proxy confirms something is there before you start blaming certificates. It is a two second check that saves a lot of misdirected effort.
What certificate is being served, right now? This is the one that matters. A TLS inspection shows the expiry, the SAN list and the issuer chain of what the proxy is serving this instant, as opposed to what your ACME client believes it wrote to disk. Check that the SAN list covers the exact hostname you are using, and that the intermediate certificate is included.
That last point produces the most confusing symptom in the category: a service that works in your desktop browser and fails on your phone and on every command line tool. Desktop browsers often paper over a missing intermediate by fetching or caching it. Nothing else does. More on that in the post on reading SSL certificate errors.
Is your clock right? Certificate validation is time based, so a device whose clock is badly wrong rejects every certificate on the network at once. This bites homelabs specifically: a Raspberry Pi has no battery backed clock, and a container or VM restored from a snapshot can come back with a time weeks out of date. An NTP time check shows the offset in milliseconds and turns a baffling failure into an obvious one.
Worth saving those four as a single automation pointed at your proxy, then running it weekly. Certificates expire quietly, and the one that takes a service down is never the one you were watching. Run History keeps the results so you can see an expiry date approaching rather than discovering it on a Sunday.
The short version
Do not build a DNS zone on .local. Buy a domain, issue one wildcard certificate for a subdomain using DNS validation, point internal DNS at private addresses, and put a single reverse proxy in front of everything. Nothing needs to be reachable from the internet for any of that, every device trusts it without configuration, and a wildcard keeps your service names out of public transparency logs. Then check what the proxy is actually serving rather than what your certificate tool says it wrote, and keep an eye on the clock.
Frequently asked questions
Can I get a real SSL certificate for a service that is not on the internet?
Yes. Certificate authorities offer a DNS based challenge where you prove control of the domain by creating a TXT record, rather than proving control of a web server. Nothing needs to be publicly reachable, so a private service can hold a publicly trusted certificate.
Why should I not use .local for my homelab domain?
It is reserved for multicast DNS, where the device itself answers rather than a DNS server. Some operating systems refuse to send .local queries to a resolver at all, so a .local DNS zone works on some devices and not others in ways that look random.
What domain should I use for internal services?
A subdomain of a real domain you own, such as home.example.com. It is the only option that allows publicly trusted certificates. The .internal suffix is formally reserved and safe from collisions, but no public authority will issue certificates for it.
Should I use a wildcard certificate for my homelab?
Yes, for two reasons. One certificate covers every service you add later with no further certificate work, and it keeps your internal service names out of public Certificate Transparency logs, which list every individually issued certificate.
Does a wildcard certificate cover all my subdomains?
Only one label deep. A certificate for *.home.example.com covers nas.home.example.com but not home.example.com itself and not a.b.home.example.com. Add the apex explicitly if you need it.
My certificate renewed but the service still serves the old one. Why?
Web servers and reverse proxies hold certificates in memory. Writing a new file on disk changes nothing until the process reloads, so make the reload part of the renewal hook and then check what the service is actually serving rather than what is on disk.
Why does my homelab site work on my laptop but fail on my phone?
The server is probably not sending the intermediate certificate. Desktop browsers often work around a missing intermediate by fetching or caching it, while phones, command line tools and other servers do not. Inspect the full chain the service sends.
Why do all my certificates suddenly look invalid?
Check the clock. Certificate validation compares against device time, and homelabs are prone to this: a Raspberry Pi has no battery backed clock, and a container or VM restored from a snapshot can return with a badly wrong time.
NetDebug Toolkit