The browser warnings about self-signed certificates on devices in my private network had become annoying. I wanted a simple way to replace those certificates with ones my browsers would trust.
Let’s Encrypt issues trusted TLS certificates free of charge. Before issuing a certificate, it verifies that you control the domain name. HTTP-01 validation requires a challenge endpoint reachable from the internet on TCP port 80. I wanted to avoid opening or forwarding that port into my private network just to issue and renew certificates for my OpenWrt access points.
I use DNS-01 validation because it does not require opening any inbound ports on my router for certificate validation.
I already own a public domain and have access to its DNS configuration, which lets me manage the DNS records needed for DNS-01 validation. I use a dedicated subdomain for my private network, such as int.foo.bar, to keep internal device names separate from my public services. This gives my devices unique names under a domain I control, instead of relying on local suffixes such as .local or .lan. My existing DNS provider does not need an API for this setup. I delegate int.foo.bar to dynv6 and use its DNS API to automate the TXT record updates needed for certificate issuance and renewal.
For my OpenWrt router, I therefore use Let’s Encrypt with DNS-01 validation through the dynv6 DNS API. acme.sh creates a temporary DNS TXT record, and Let’s Encrypt checks it to verify domain control. This removes the need for inbound port 80 access during validation.
Why another DynDNS provider?
- My current DNS provider did not offer a DNS API and I needed a reliable way to automate my TLS certificate renewals.
- I also want to make some hosts reachable using dynamic IPv6 addresses. This is a separate use of dynv6; dynamic IP address updates are not required for DNS-01 validation.
This walkthrough targets OpenWrt 25.12 with apk. All domain names, addresses and credentials below are placeholders.
1. Delegate a subdomain to dynv6
Assume you own foo.bar and want the router in your network to use router.int.foo.bar. To do this, you will need to add the following three NS records pointing to your dynamic DNS provider. This will delegate responsibility for the subdomain int.foo.bar to the dynamic DNS provider, so it can manage all subsequent requests properly.
At the DNS provider hosting foo.bar, open the DNS record editor and add:
| Name | Type | Target | TTL |
|---|---|---|---|
int | NS | ns1.dynv6.com. | 3600 |
int | NS | ns2.dynv6.com. | 3600 |
int | NS | ns3.dynv6.com. | 3600 |
If the editor expects a full name, use int.foo.bar. The trailing dot marks an absolute target name; some providers add it automatically.
These records delegate int.foo.bar and everything below it. Keep the existing nameservers for foo.bar.
In dynv6, register int.foo.bar as your delegated domain (MyDomains with option Multiple zones), then create the zone router.int.foo.bar beneath it. Complete dynv6’s delegation check. Adding NS records at your provider does not automatically create the corresponding configuration in dynv6.
From your workstation, inspect the delegation: run the dig command to verify the name server records are resolving correctly.
dig +trace NS int.foo.bar
The parent’s referral should list the three dynv6 nameservers. Allow existing DNS caches to expire before troubleshooting ACME.
Configure your internal DNS to resolve router.int.foo.bar to the router’s LAN address, for example 192.168.50.1. Public A or AAAA records pointing to the router are unnecessary for DNS-01.
2. Install the OpenWrt packages
Before changing anything, back up /etc/config/uhttpd, its current certificate and key, and any existing ACME configuration. Keep SSH access open.
On the router, as root:
apk update &&
apk add acme-acmesh acme-acmesh-dnsapi luci-app-acme
The router needs correct system time and outbound access to DNS, dynv6 and Let’s Encrypt. To ensure successful certificate generation, verify these settings before proceeding.
3. Configure ACME in LuCI
- Obtain an HTTP API token from dynv6 Keys that can manage your router’s zone.
- Assign a separate token for each zone.
- Ensure you save and apply these changes before proceeding.
Open Services → ACME certificates, enter your real account email, and add a certificate configuration:
| Setting | Value |
|---|---|
| Enabled | Yes |
| Validation method | DNS-01 |
| Domain names | router.int.foo.bar |
| DNS API | dns_dynv6 |
| DNS API credentials | DYNV6_TOKEN=REPLACE_WITH_YOUR_TOKEN |
| Challenge / Domain Alias | Leave empty |
| Wait for DNS update | 120 seconds |
| Key type | ECC 384 / P-384 |
| ACME server | Let’s Encrypt |
| Use staging server | Disabled for a trusted certificate |
If LuCI provides a dedicated token field, enter the token there instead. The acme.sh dynv6 driver uses DYNV6_TOKEN for REST API authentication.
Follow the settings in the table above: check Enabled and use Let’s Encrypt as the ACME server. Leave the custom ACME server URL empty when the default is letsencrypt; do not enter the Buypass URL shown in the example field.



Save and apply. Watch the log until issuance completes. A message about adding a TXT record alone does not mean the certificate was issued.
4. Configure LuCI’s web server
After successful issuance, point uhttpd at the certificate and private key:
(
set -e
cert_dir='/etc/ssl/acme'
name='router.int.foo.bar'
test -s "$cert_dir/$name.fullchain.crt"
test -s "$cert_dir/$name.key"
uci set uhttpd.main.cert="$cert_dir/$name.fullchain.crt"
uci set uhttpd.main.key="$cert_dir/$name.key"
uci commit uhttpd
/etc/init.d/uhttpd restart
)
Use the stable paths under /etc/ssl/acme. The OpenWrt package creates the links to acme.sh’s working files.
From your workstation, verify HTTPS with certificate validation enabled:
curl -I --connect-timeout 5 --max-time 15 \
https://router.int.foo.bar/
Open LuCI using that hostname. Accessing it by private IP will still produce a certificate name mismatch.
5. Enable renewal
On the router:
/etc/init.d/acme enable
/etc/init.d/acme start
grep -n acme /etc/crontabs/root
Check that a renewal job exists and is not duplicated. OpenWrt 25.12’s uhttpd service handles the acme.renew event, so it can reload renewed certificates.
If validation fails, check the public delegation, zone name and token permissions first. If issuance succeeds but HTTPS fails, check internal DNS and uhttpd’s certificate paths.
The original setup was tested with OpenWrt 25.12.5 and acme-acmesh 3.1.3-r3.