Stop renewing your pfSense certificate by hand, even when your DNS host has no API

6 August 2026

If your registrar has no API that your ACME client supports, the usual advice is to move your domain somewhere else. You do not have to. One CNAME record, created once, is enough to make renewal fully automatic and keep your DNS exactly where it is.

I had been renewing a pfSense web GUI certificate by hand for years: wait for the reminder, log into the DNS panel, paste a TXT record, click renew, delete the record. Every 60 to 90 days. It took about ten minutes and it worked, until the day it nearly did not, and the certificate came within six days of expiry because nobody happened to look.

Here is the fix, and the two traps that bit me on the way.

Why the manual loop happens

Let’s Encrypt offers two common ways to prove you control a domain. HTTP-01 needs port 80 reachable from the internet. DNS-01 needs a TXT record at _acme-challenge.<your-domain>, and is the better choice for a firewall or anything else you would rather not expose.

DNS-01 is only automatic if your ACME client can write that TXT record for you, which means your DNS provider needs a supported API. Plenty of perfectly good registrars do not have one. When that happens, the pfSense ACME package falls back to the manual DNS method, and the nightly renewal job does this:

DNS record not yet added. Will save to … and exit.

Please add the TXT records to the domains, and re-run with –renew.

That is not an error you will notice. The cron job runs, prints its request into a log nobody reads, and exits cleanly. Renewal silently depends on a human. Mine had been failing every night for weeks before I looked.

The trick: delegate only the challenge name

A TXT record for validation does not have to live in your real domain’s zone. It only has to be *reachable* from it. DNS gives you a redirect primitive for exactly this, the CNAME.

So: put a permanent CNAME at the challenge name in your no-API zone, pointing at a challenge name in a zone you *can* drive by API. Your ACME client writes the TXT record in the second zone. Let’s Encrypt follows the CNAME and finds it.

_acme-challenge.host.example.com. CNAME _acme-challenge.host.example.net.

Your real domain stays where it is. Its nameservers do not change. No other records move. You create one record, once, and never touch it again. Every future renewal writes into the other zone automatically.

acme.sh calls this a challenge alias, and the pfSense ACME package exposes it as a per-domain field called Challenge Alias.

What you need

• A zone you can drive by API. Route53, Cloudflare, and about a hundred others are supported by acme.sh. A Cloudflare account is free if you have nothing suitable, and any domain you already own will do.

• One CNAME record at your existing DNS host.

• An API credential, which you should scope tightly. More on that below.

Setting it up

1. Create a scoped API credential

Do not use an admin key. Your firewall only needs to write one TXT record at one name, so grant exactly that.

Route53 supports condition keys that make this precise:

{

“Effect”: “Allow”,

“Action”: [“route53:ChangeResourceRecordSets”],

“Resource”: “arn:aws:route53:::hostedzone/ZONEID”,

“Condition”: {

“ForAllValues:StringEquals”: {

“route53:ChangeResourceRecordSetsNormalizedRecordNames”:

[“_acme-challenge.host.example.net”],

“route53:ChangeResourceRecordSetsRecordTypes”: [“TXT”]

}

}

}

Add route53:ListHostedZones, ListHostedZonesByName and GetChange on *, and ListResourceRecordSets on that zone, and you are done. On Cloudflare, the equivalent is an API token scoped to Zone.DNS edit on a single zone.

Then test that the scoping actually works. A policy you have not tested is a guess. Run one call that should succeed and several that should fail:

Test Expected
Write TXT at the permitted name allowed
Write TXT at a different name in the same zone denied
Write a different record type at the permitted name denied
Touch a different zone entirely denied

If all four behave, a leaked firewall credential can create one worthless ACME record and nothing else. That is a meaningful difference from an admin key sitting in a config file.

2. Create the CNAME, and delete the old TXT first

At your existing DNS host:

Field Value
Name _acme-challenge.host
Type CNAME
Target _acme-challenge.host.example.net.

Delete any existing TXT record at that name before you create the CNAME. DNS does not allow a CNAME to coexist with another record type at the same name. If you have been renewing manually, there is almost certainly a stale TXT record sitting there from the last time.

Then wait, and check with a low-level tool rather than a browser:

dig +short CNAME _acme-challenge.host.example.com @1.1.1.1

Give it a few minutes. Mine had a 247-second TTL and I checked too early, concluded it had not been created, and started looking for a mistake that was not there.

Which brings me to a habit worth borrowing: before you believe a negative result, prove your check can produce a positive one. Query something you know exists, using the same command and the same server. If that also comes back empty, your check is broken, not your DNS. That one habit has saved me more time than any tool.

3. Point the ACME client at it

In pfSense: Services, Acme Certificates, edit the certificate, and on the domain row set the method to your DNS provider, paste the credential, and put the alias domain in Challenge Alias. The alias is the bare name; acme.sh prepends _acme-challenge. itself. So for the CNAME above, the alias is host.example.net.

While you are in there, check for duplicate domain rows. Mine had the same hostname listed twice, so every renewal ran the validation twice. Easy to acquire by clicking Add once too often, and invisible until you look.

4. Renew, and check the serial number

Force a renewal. The log should show validation happening without you:

dvlist=‘host.example.com#verified_ok##dns-01#dns_aws#…’

Cert success.

Then verify what is actually being served:

echo | openssl s_client -connect host.example.com:443 2>/dev/null \

| openssl x509 -noout -serial -enddate

Check the serial number, not just the expiry date. This matters more than it sounds, as the next section explains.

The trap: your new certificate may not be the one being served

This is the part I would have missed entirely if I had checked the obvious thing.

The renewal succeeded. The certificate store had the new certificate. The log said Cert success and Reload successful. And the firewall was still serving the old certificate, because on pfSense the file nginx reads, /var/etc/cert.crt, is only regenerated when the web GUI restarts.

Both certificates expired on the same date, because I had renewed manually that morning. So an expiry check showed exactly what I wanted to see while the old certificate was still live. Only the serial numbers disagreed:

in the certificate store: 0626A5A1D54B800E11330B2B018876AD8B9E

actually being served: 0533D4D0D5A34CA1960ADFC89654BF19E154

A check that cannot fail is not a check. Expiry dates are a weak signal here because a renewed and a not-yet-replaced certificate can share one. Serial numbers are unique per certificate, so they answer the real question.

Closing the loop: make the restart automatic

Fixing it by hand once is not enough, because the same thing will happen at every unattended renewal. The pfSense ACME package runs an action list after it stores a certificate, so give it the job:

Field Value
Status enable
Method shellcommand
Command /etc/rc.restart_webgui

Other ACME clients have the same idea under different names: acme.sh has –reloadcmd, certbot has –deploy-hook. If you run any service that reads a certificate from disk, it needs a reload after renewal, and the hook is where that belongs.

Test it without waiting 60 days, and without burning your rate limit on a real renewal. Put a deliberately wrong certificate in place of the live one, run the exact command your hook will run, and confirm the correct certificate comes back. I generated a throwaway self-signed certificate, dropped it in, ran the command, and watched the real one return. That proves the hook does the job, rather than proving the command exits zero.

What this leaves you with

One CNAME at your old DNS host. One tightly scoped API credential. One hook to reload the service. After that the nightly job renews on its own at about 60 days, and the only thing left to do is nothing.

If you want to confirm it worked when the first automatic renewal comes round, check the serial has changed. That is the whole maintenance burden now.

A checklist

1. Pick a zone you can drive by API. Free tiers are fine.

2. Create a credential scoped to one record name and the TXT type. Test it with deliberate failures, not just a success.

3. Delete the stale TXT at the challenge name in your real zone.

4. Create the CNAME pointing at the API zone. Confirm with dig, after a wait, with a positive control.

5. Set the ACME client’s challenge alias. Remove duplicate domain rows.

6. Add a reload hook for whatever serves the certificate.

7. Renew once by hand and verify the serial changed, from an external client.

8. Note the date 60 days out and check the serial once more. Then forget about it.