Home network devices with a self-signed certificate warning changing to a trusted certificate with a padlock and checkmark.

OpenWrt: Trusted HTTPS for LuCI with Let’s Encrypt and dynv6

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:

NameTypeTargetTTL
intNSns1.dynv6.com.3600
intNSns2.dynv6.com.3600
intNSns3.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:

SettingValue
EnabledYes
Validation methodDNS-01
Domain namesrouter.int.foo.bar
DNS APIdns_dynv6
DNS API credentialsDYNV6_TOKEN=REPLACE_WITH_YOUR_TOKEN
Challenge / Domain AliasLeave empty
Wait for DNS update120 seconds
Key typeECC 384 / P-384
ACME serverLet’s Encrypt
Use staging serverDisabled 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.

LuCI ACME general settings showing DNS-01 validation for router.int.foo.bar.
LuCI DNS challenge settings showing the dynv6 API, a dummy REST API token and a 120-second DNS update wait.
LuCI advanced ACME settings showing an ECC 384-bit key, the ACME server URL field and staging mode disabled.

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.