TL;DR

  1. 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.
  2. pfSense → System → Package Manager → install acme and haproxy.
  3. Services → ACME → Account keys → Add: ACME Server Let’s Encrypt Production ACME v2 (or Staging to test) → Create new account key → Register → Save.
  4. Services → ACME → Certificates → Add: name, the account, Domain SAN list: app.example.com (and *.example.com if 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.
  5. 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 fieldValueWhere
TokenThe API tokenMy Profile → API Tokens
Account IDAccount IDZone → Overview → API (right column)
Zone IDZone IDSame place
Key / EmailEmptyOnly 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.com wildcard the validation has to be DNS-01 (HTTP-01 does not allow it). A wildcard does not cover the apex: add example.com as 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 to 120.
  • 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:

  1. Settings: enable HAProxy, and leave Max SSL Diffie-Hellman size at 2048 or more.
  2. 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.
  3. Save → Apply Changes.

Test from outside:

1
2
openssl s_client -connect app.example.com:443 -servername app.example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -dates

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 A record is proxied, HAProxy sees Cloudflare’s IP as the source. For an internal service behind pfSense, grey cloud.
  • Issue fails with Invalid request headers or Authentication 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_client that notAfter moved.
  • 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).