Farrier

DNS providers and CAs

Appliances behind a firewall can't answer an HTTP challenge, so Farrier proves domain control with DNS-01. It writes the _acme-challenge TXT record through your DNS provider's API and waits until every authoritative nameserver serves it. There is no fixed sleep. Records are always cleaned up, including when validation fails.

Providers

Providerprovider
Cloudflarecloudflare
AWS Route 53 (with STS AssumeRole for other accounts)route53
Azure DNS (whole subscription)azure
Google Cloud DNSgoogle
RFC 2136 dynamic update with TSIG (BIND, Knot, Windows DNS)rfc2136
ACME-DNS (CNAME delegation)acme-dns
DigitalOcean, Linode, Vultr, Hetzner DNS, Hetzner Cloud, Scalewaydigitalocean, linode, vultr, hetzner, hetzner-cloud, scaleway
PowerDNS, deSEC, NS1, DNS Made Easy, Akamai Edge DNSpowerdns, desec, ns1, dnsmadeeasy, akamai
Gandi, OVH, Namecheap, Porkbun, GoDaddy, Dynu, Infomaniak, ArvanCloudgandi, ovh, namecheap, porkbun, godaddy, dynu, infomaniak, arvancloud
DuckDNS, Hurricane Electricduckdns, he (one TXT value per name, so no apex plus wildcard in one order)
EfficientIP SOLIDserversolidserver
Anything else, through your own scriptscript

Tested against real APIs?

Cloudflare, Route 53, Azure, Google Cloud DNS, DigitalOcean, ACME-DNS and RFC 2136 have request-level tests. The others follow each provider's published API. Try a staging certificate first, and open an issue if one misbehaves.

Accounts

Each entry in <data>/dns.json is a named account. You can have several per provider: prod and staging Cloudflare, several AWS accounts, several Azure subscriptions.

dns.json
{
  "cf-prod": {
    "provider": "cloudflare",
    "credentials": { "api_token": "env:FARRIER_SECRET_CF_PROD" },
    "zones": ["example.com"]
  },
  "aws-validation": {
    "provider": "route53",
    "credentials": { "access_key_id": "env:FARRIER_SECRET_AWS_ID", "secret_access_key": "env:FARRIER_SECRET_AWS_KEY" },
    "options": { "role_arn": "arn:aws:iam::222222222222:role/farrier-dns01" },
    "zones": ["acme-validation.example.org"]
  },
  "bind-internal": {
    "provider": "rfc2136",
    "credentials": { "tsig_secret": "file:/run/secrets/tsig-farrier.b64" },
    "options": { "server": "ns1.internal.example", "tsig_key": "farrier-key", "tsig_algorithm": "hmac-sha256" },
    "zones": ["internal.example"]
  }
}

Credentials are always env: or file: references. Inline values are refused when the file loads, and secrets are resolved only when a request is made, never written to disk or into an error message. More examples are in examples/dns.json.

Which account is used:

  1. An endpoint's dns_account (in its connector config), or the issuer's "dns": "<account>".
  2. Otherwise, with "dns": "auto", the account whose zone matches the name most specifically. Matching is by whole labels, so badexample.com never matches example.com. Two accounts claiming one zone is an error, not a guess.

CNAME delegation. Point _acme-challenge.app.example.com at a record in a validation zone (CNAME app.acme-validation.example.org). Farrier follows the CNAME and writes through the account that owns the target zone, so production DNS credentials never need to be on the engine.

The existing dns_hook script setting keeps working unchanged.

Certificate authorities

An issuer in <data>/issuers.json can name a preset:

issuers.json
{
  "le":       { "preset": "letsencrypt", "email": "pki@example.com", "dns": "auto" },
  "zerossl":  { "preset": "zerossl", "eab_kid": "env:FARRIER_SECRET_ZEROSSL_KID",
                "eab_hmac_key": "env:FARRIER_SECRET_ZEROSSL_HMAC", "dns": "auto" },
  "internal": { "type": "local_ca" }
}
PresetCAExternal Account Binding
letsencrypt, letsencrypt-stagingLet's Encryptno
zerosslZeroSSLyes
google, google-stagingGoogle Trust Servicesyes
buypass, buypass-stagingBuypass Gono
sslcom, sslcom-rsaSSL.comyes
digicert, sectigoDigiCert, Sectigo (give directory_url)yes

Use several issuers at once and pick one per endpoint, for example a public CA for internet-facing VIPs and the built-in local_ca for management interfaces.

Let's Encrypt

Farrier follows Let's Encrypt's own client guidance:

  • Renewal timing from the CA (ARI, RFC 9773). For certificates Farrier issued, it asks Let's Encrypt for a suggested renewal window and renews inside it when that is earlier than its own schedule. This is how Let's Encrypt moves renewals forward during an incident or a mass revocation. New orders name the certificate they replace, which Let's Encrypt exempts from some rate limits.
  • Rate limits. A 429 rateLimited error is recorded with the time Let's Encrypt says to retry, and the renewal is retried later. A brief 503 is retried once. Polling follows Retry-After.
  • Staging first. Use the letsencrypt-staging preset until a full renewal works, then switch to letsencrypt.
issuers.json
{
  "le": {
    "preset": "letsencrypt",
    "email": "pki@example.com",
    "dns": "auto",
    "profile": "tlsserver",
    "preferred_chain": "ISRG Root X1"
  }
}
OptionMeaning
profileclassic (default), tlsserver, or shortlived for 6-day certificates. Checked against the profiles the CA offers.
preferred_chainThe root or issuer name of the chain to serve, picked from the alternates the CA offers. Falls back to the default chain.

The client is tested in CI against Pebble, Let's Encrypt's own ACME test server, with real DNS-01 validation.