TL;DR
- Cloudflare → My Profile → API Tokens → Create Token → Edit zone DNS template: permissions Zone → Zone → Read and Zone → DNS → Edit, Zone Resources limited to your zone. Copy the token; the Account ID and Zone ID are on the zone’s Overview page.
- pfSense → System → Package Manager → install acme and haproxy.
- Services → ACME → Account keys → Add: ACME Server Let’s Encrypt Production ACME v2 (or Staging to test) → Create new account key → Register → Save.
- Services → ACME → Certificates → Add: name, the account, Domain SAN list:
app.example.com(and*.example.comif you want a wildcard) with method DNS-Cloudflare, and in its fields only Token, Account ID and Zone ID. Actions list: Restart Local Service →haproxy. Save → Issue. - Services → HAProxy → Frontend → edit → SSL Offloading ✔ on the listen address → Certificate: the one ACME just created → Save → Apply Changes.
Automatic renewal (the package’s daily cron) and HAProxy restarted on every renewal.
Cloudflare: the token
The Global API Key old guides ask for gives access to the whole account; an API token with minimal permissions is the right thing and is what the package’s DNS-Cloudflare method uses when you fill in Token:
| Package field | Value | Where |
|---|---|---|
| Token | The API token | My Profile → API Tokens |
| Account ID | Account ID | Zone → Overview → API (right column) |
| Zone ID | Zone ID | Same place |
| Key / Email | Empty | Only for the Global API Key (do not use it) |
With Token one of the two IDs is enough; setting both does no harm. Token permissions: Zone:Read and DNS:Edit on the specific zone (Include → Specific zone).
If you enable Client IP Address Filtering on the token, it must include the firewall’s public IPv4 and also IPv6: pfSense goes out through whichever it has, and a token limited to v4 fails as soon as the API call goes over v6.
ACME: account and certificate
Account. Create two: one against Staging to test and one against Production. Staging has no issuance limits but its certificates are not trusted; once everything works, switch the certificate’s account to production and issue again.
Certificate. Under Services → ACME → Certificates → Add:
- Domain SAN list: one entry per name. For a
*.example.comwildcard the validation has to be DNS-01 (HTTP-01 does not allow it). A wildcard does not cover the apex: addexample.comas a second entry. - Method: DNS-Cloudflare on each entry, with the three fields.
- DNS Sleep: Cloudflare propagates in seconds; the default is plenty. If issuance fails with
TXT not found, raise it to120. - Key: RSA 2048 or ECDSA 256; both work fine with HAProxy.
- Actions list: Restart Local Service → haproxy. Without it, HAProxy keeps serving the old certificate until someone restarts it, even though ACME has already renewed.
Issue from the certificate list. The result shows up under System → Certificates with the name you gave it; that is where HAProxy will see it.
HAProxy: using the certificate
Services → HAProxy:
- Settings: enable HAProxy, and leave Max SSL Diffie-Hellman size at 2048 or more.
- Frontend → your HTTPS frontend → External address: port
443, SSL Offloading ✔ → SSL Offloading section → Certificate: the ACME one. For several domains on the same frontend, Additional certificates with the rest; HAProxy picks by SNI. - Save → Apply Changes.
Test from outside:
| |
issuer=C = US, O = Let's Encrypt and notAfter 90 days out.
As a bonus, the pfSense GUI with the same certificate: System → Advanced → Admin Access → SSL/TLS Certificate, and a second action on the ACME certificate: Shell Command /etc/rc.restart_webgui.
Traps
- Cloudflare in proxy mode (orange cloud) does not affect DNS-01, but it does affect how traffic arrives: if the
Arecord is proxied, HAProxy sees Cloudflare’s IP as the source. For an internal service behind pfSense, grey cloud. Issuefails withInvalid request headersorAuthentication error: token without permission on that zone, token IP filter, or the Zone ID of another zone.- Production rate limits: five certificates per set of names per week. Test on staging.
- The certificate renews; HAProxy does not reload it by itself. The restart action is mandatory. Check under Services → ACME that the last renewal has a date and in
openssl s_clientthatnotAftermoved. - ACME certificates renew at 2/3 of their lifetime (about 60 days). If you see the cron running and nothing changes before then, that is normal.
Reference: ACME package in the Netgate documentation and acme.sh’s DNS provider list (dns_cf section).