Skip to content
Bytearium

No ads or affiliate links active

Networking

Homelab HTTPS: Let's Encrypt via DNS-01

Homelab HTTPS with Let's Encrypt via DNS-01: wildcard certificates without open ports, scoped API tokens, local DNS, CAA and renewal.

Every homelab has that one service where everyone has learned to click "Accept the risk and continue". Trained click-through is exactly the habit an attacker loves. The good news: your internal services can get real certificates without opening a single port.

The short answer

How do internal homelab services get a trusted certificate without a port open to the internet? You use a domain you own and let an ACME client prove control of it with the DNS-01 challenge: the client writes a TXT record at _acme-challenge, Let's Encrypt checks it in public DNS, and then it issues the certificate [1][2]. With DNS-01 you can get a wildcard such as *.home.example.com, serve it from your reverse proxy and resolve the names to private addresses inside your network only. The price: you need a DNS provider with an API, the API token becomes a valuable secret, and every name in a certificate ends up in public logs. All facts are as of 25 Oct 2026. In this guide, "homelab" means the servers and services you run at home for yourself.

How it works

  1. Why .local and .internal do not work. Public certificate authorities only issue for names in the public DNS that you control. Caddy's documentation lists localhost, .local, .internal and .home.arpa among the names that do not qualify for publicly trusted certificates [11]. So you need a real domain. A subdomain like home.example.com is enough, and it keeps homelab names apart from your website.
  2. The challenge, step by step. The ACME protocol is standardised in RFC 8555 [2]. Your client creates an account key and orders a certificate. For each name, the CA hands out a token. The client combines the token with the fingerprint of its account key (the "key authorization"), hashes it with SHA-256, encodes it in base64url and publishes the result as a TXT record at _acme-challenge.<name> [2]. Let's Encrypt queries DNS for that record. If the value matches, the name counts as validated [1]. Nothing has to reach your network, because only the DNS provider's servers answer.
  3. Why DNS-01 and not HTTP-01. HTTP-01 needs port 80 reachable from the internet, and neither HTTP-01 nor TLS-ALPN-01 can issue wildcard certificates [1]. DNS-01 can do both: wildcards, and names whose servers are not exposed to the public internet [1]. It cannot validate IP addresses, though [1].
  4. What a wildcard covers. A wildcard may only stand as the complete left-most label, and it matches exactly one label [3]. So *.home.example.com covers nas.home.example.com, but neither home.example.com itself nor a.b.home.example.com [3]. If you want the bare name too, put both in the order, as the examples of acme.sh and lego do with -d example.com -d '*.example.com' [16][18]. Both names then need their own TXT value under the same _acme-challenge name. Several TXT records may exist there, but clean up old ones, because Let's Encrypt rejects responses that grow too big [1].
  5. Keep the API token small. Let's Encrypt warns that full DNS API credentials on a web server raise the damage if that server is hacked. It recommends narrowly scoped credentials, or running the validation on a separate machine and copying the certificates over [1]. In practice: create a token that may only edit DNS records, only in this one zone, and add an IP filter if your provider offers one. The acme.sh documentation for Cloudflare, as one example, names exactly that minimum: "Zone > DNS > Edit" for the specific zone, and it discourages the global API key [18]. Keep the token out of configuration files that end up in Git. Caddy reads it from an environment variable, lego from an env file, and acme.sh stores it in its account.conf, so protect that file [12][16][18].
  6. Delegate the challenge if your provider is weak. Let's Encrypt follows CNAME and NS records when it looks up the TXT record. You can therefore point _acme-challenge.home.example.com to a separate validation zone at a provider with a good API [1]. Your main zone then needs no API token at all. Caddy has dns_challenge_override_domain for this; Traefik calls CNAME support "supported and even encouraged" [12][15].
  7. Plan for propagation. DNS providers need time until a new record is visible on all their servers. Without an API that reports this, Let's Encrypt suggests waiting long enough, often as much as an hour [1]. Clients check for the record before they ask for validation. Caddy waits up to 2 minutes by default (propagation_timeout), Traefik has propagation.delayBeforeChecks, lego has --dns.propagation.wait [12][15][17]. One homelab trap: if your internal resolver answers for your own zone, the propagation check may see the internal answer instead of the public one. Point the check to public resolvers with Caddy's resolvers or Traefik's dnsChallenge.resolvers [12][15]. That is our reading of these options, not a statement from the projects.
  8. Pick a client. Caddy needs a build that includes the module for your DNS provider from the caddy-dns repositories. You download it from the Caddy site or build it with xcaddy [12][14]. Traefik uses lego internally and lists its DNS providers with the environment variables each one needs [15]. acme.sh is a shell script with its own DNS API scripts, lego a single Go binary [16][18]. Our assessment: if you already run one of these proxies, use its built-in ACME support. A separate client pays off when other devices also need the certificate.
  9. Resolve the names locally. The public zone needs only the _acme-challenge records. Your internal resolver (router, Pi-hole, AdGuard Home or Unbound) answers *.home.example.com with the private address of your reverse proxy. This keeps internal addresses out of public DNS. Our assessment: this split is the cleaner choice. If you put private addresses in public DNS instead, DNS rebinding protection gets in the way. Unbound removes private addresses from answers for public names when you set private-address, and lets you allow single domains with private-domain. Records you configure locally with local-data are allowed by default [19]. OPNsense offers the same as "Private Domains" and "Rebind Protection networks" [20]. A FRITZ!Box suppresses such answers too. You add exceptions as full host names under Home Network > Network > Network Settings, in the "DNS Rebind Protection" section; depending on the FRITZ!OS version, it sits behind the advanced settings [21][22].
  10. Lifetimes and renewal. Let's Encrypt's default certificates are valid for 90 days. Short-lived certificates of about 6 days are optional, and the tlsserver profile has issued 45-day certificates since 13 May 2026 [5][6][7]. On 10 Feb 2027 the default drops to 64 days, on 16 Feb 2028 to 45 days. The period in which a successful validation can be reused shrinks from 30 days to 10 days and then to 7 hours [7][8]. Let's Encrypt recommends ACME Renewal Information (ARI), which tells the client when to renew. A fixed renewal after 60 days will no longer be enough [7]. lego uses ARI unless you switch it off [17]. Traefik assumes 2,160 hours (90 days) with certificatesDuration to plan renewals [15]. Our reading: if you choose a shorter profile there, adjust that value. Let's Encrypt stopped its expiry emails on 4 Jun 2025 [8], so monitor expiry dates yourself.
  11. Rate limits and staging. As of 25 Oct 2026, Let's Encrypt allows 50 certificates per registered domain and 5 per identical set of names every 7 days, plus 5 failed validations per name and account per hour [4]. Renewals via ARI are exempt from all rate limits. A renewal without ARI (same set of names) is exempt from the per-domain limit, but still counts towards the 5 per set and the failed validations [4][9]. Test with the staging environment, which has much higher limits [4]. Caddy (ca), Traefik (caServer) and lego (--server) let you switch the endpoint [12][15][17].
  12. CAA as a lock on the door. A CAA record names the CAs that may issue for your domain. issue "letsencrypt.org" allows Let's Encrypt; the parameter validationmethods=dns-01 restricts it to DNS validation, and accounturi restricts it to your ACME account [10]. The CA looks for the CAA record closest to the name and follows CNAMEs [10].
  13. Names become public. Issued certificates appear in public Certificate Transparency logs, and Let's Encrypt points to search tools such as crt.sh [4]. A certificate for nas.home.example.com tells the world that the name exists. Our reading: a wildcard keeps the individual service names out of the certificate.
  14. The alternative: your own CA. Without a domain, or in a network that must stay offline, Caddy can run its own local CA. It needs no ACME and no DNS, but every device has to trust its root [11]. Our reverse proxy guide compares both options in more detail.

A worked example (example.com and the addresses are placeholders)

The setup: a reverse proxy on a private address, a domain example.com, homelab names under home.example.com. No port is forwarded.

WhereRecordPurpose
Public DNSexample.com CAA 0 issue "letsencrypt.org;validationmethods=dns-01"Only Let's Encrypt, only via DNS-01 [10]
Public DNS_acme-challenge.home.example.com TXT ...Set and removed by the ACME client
Internal resolver*.home.example.com → 192.168.1.10Names point to the proxy, invisible from outside

The Caddyfile follows the wildcard pattern from the Caddy documentation. Cloudflare is the example provider in Caddy's own docs, not our recommendation, and every provider module has its own syntax [12][13]:

*.home.example.com {
	tls {
		dns cloudflare {env.CLOUDFLARE_API_TOKEN}
		resolvers 1.1.1.1
	}

	@nas host nas.home.example.com
	handle @nas {
		reverse_proxy 192.168.1.20:5000
	}

	handle {
		abort
	}
}

abort closes connections for names you have not configured [13]. The token sits in an environment variable, not in the file. For a first run, set the staging endpoint and switch to production once it works [12]. With lego, the same order with the token in a separate env file looks like this, again with the provider from the lego docs. Since lego v5, all flags follow run [16][17]:

lego run --server https://acme-staging-v02.api.letsencrypt.org/directory --dns cloudflare --domains 'home.example.com' --domains '*.home.example.com' --env-file .env.dns

We did not run these configurations against a live provider. They follow the documentation, and options can change between versions.

Checklist

  • Choose a subdomain of a domain you own for all homelab names.
  • Check that your DNS provider has an API and is supported by your client, or delegate _acme-challenge by CNAME.
  • Create a token that may only edit DNS records in this zone, and store it outside Git.
  • Test with the Let's Encrypt staging environment before you switch to production.
  • Request the wildcard and, if needed, the bare name in the same order.
  • Set local DNS records for the names, and allow the domain in your rebinding protection only if you publish private addresses.
  • Add a CAA record with letsencrypt.org and validationmethods=dns-01.
  • Turn on ARI or a renewal schedule that copes with 45-day certificates, and monitor expiry dates.
  • Remember that every name in a certificate is public, and keep internal details out of names.

Related pages: Reverse proxy for a homelab; DNS; Zero trust; DNS ad blocking: Pi-hole or AdGuard Home; DDNS; WireGuard or Tailscale for homelab access; Uptime Kuma.

Primary sources

These sources are the basis of this page. That does not mean every claim was checked individually; whether a review record exists is shown above under “Status”. The interpretation and checklist are our own work.

There is no documented review record for this version yet. The text relies on the sources listed.

How this page was made: Bytearium creates these pages with the help of AI and is responsible for publishing them. AI can be wrong, so we show our sources and review status openly.

  1. Let's EncryptChallenge Types
  2. IETFRFC 8555, Automatic Certificate Management Environment (ACME)
  3. IETFRFC 9525, Service Identity in TLS
  4. Let's EncryptRate Limits
  5. Let's EncryptCertificate Lifetime Rationale and Plans
  6. Let's EncryptProfiles
  7. Let's EncryptDecreasing Certificate Lifetimes to 45 Days
  8. Let's EncryptUpcoming Features
  9. Let's EncryptShorter Certificate Lifetimes and Rate Limits
  10. Let's EncryptCertification Authority Authorization (CAA)
  11. Caddy projectAutomatic HTTPS
  12. Caddy projecttls (Caddyfile directive)
  13. Caddy projectCommon Caddyfile Patterns
  14. Caddy communityHow to use DNS provider modules in Caddy 2
  15. Traefik LabsTraefik and ACME Certificates Resolver
  16. lego projectDNS Providers
  17. lego projectCommands & Flags
  18. acme.sh projectdnsapi
  19. NLnet Labsunbound.conf(5)
  20. OPNsense projectUnbound DNS
  21. FRITZ! (AVM)No DNS resolution of private IP addresses
  22. FRITZ! (AVM)Cannot access other devices in the FRITZ! home network

This page has no ads and no affiliate links.