Configuration: DNS providers

The tls-acme module uses DNS providers to solve the DNS-01 ACME challenge. This is the only challenge type that supports wildcard certificates. You configure a provider inside the dns { } block nested within a tls { } block. Select it by name with the provider directive.

*.example.com {
    tls {
        provider acme
        challenge dns-01
        contact "admin@example.com"
        dns {
            provider cloudflare
            api_key "{{env.CF_API_TOKEN}}"
            # provider-specific directives...
        }
    }
}

All DNS provider implementations are currently part of the dns-stalwartlite module, except the command provider, which ships in the separate dns-command module.

Providers⁠#

Bunny.net⁠#

Provider name: bunny

DirectiveArgumentsDescriptionDefault
api_key<string>Bunny DNS API key.none (required)

Configuration example:

*.example.com {
    tls {
        provider acme
        challenge dns-01

        dns {
            provider bunny
            api_key "YOUR_BUNNY_API_KEY"
        }
    }
}

Cloudflare⁠#

Provider name: cloudflare

DirectiveArgumentsDescriptionDefault
api_key<string>Cloudflare API token (scoped token).none (required)

Configuration example:

*.example.com {
    # Scoped API token (recommended)
    tls {
        provider acme
        challenge dns-01

        dns {
            provider cloudflare
            api_key "YOUR_CLOUDFLARE_API_TOKEN"
        }
    }
}

deSEC⁠#

Provider name: desec

DirectiveArgumentsDescriptionDefault
auth_token<string>deSEC API token.none (required)

Configuration example:

*.example.com {
    tls {
        provider acme
        challenge dns-01

        dns {
            provider desec
            auth_token "YOUR_DESEC_API_TOKEN"
        }
    }
}

DigitalOcean⁠#

Provider name: digitalocean

DirectiveArgumentsDescriptionDefault
auth_token<string>DigitalOcean personal access token (OAuth token).none (required)

Configuration example:

*.example.com {
    tls {
        provider acme
        challenge dns-01

        dns {
            provider digitalocean
            auth_token "YOUR_DO_OAUTH_TOKEN"
        }
    }
}

DNSimple⁠#

Provider name: dnsimple

DirectiveArgumentsDescriptionDefault
oauth_token<string>DNSimple OAuth token.none (required)
account_id<string>DNSimple account ID.none (required)

Configuration example:

*.example.com {
    tls {
        provider acme
        challenge dns-01

        dns {
            provider dnsimple
            oauth_token "YOUR_DNSIMPLE_TOKEN"
            account_id "12345"
        }
    }
}

Google Cloud DNS⁠#

Provider name: googlecloud

DirectiveArgumentsDescriptionDefault
service_account_json<string>Contents of the Google Cloud service account JSON key file.none (required)
project_id<string>Google Cloud project ID.none (required)
managed_zone<string>Name of the Cloud DNS managed zone. Ferron resolves the zone automatically if omitted.none (optional)
private_zone<bool>Set to true to target a private zone.false
impersonate_service_account<string>Service account email to impersonate.none (optional)

Configuration example:

*.example.com {
    tls {
        provider acme
        challenge dns-01

        dns {
            provider googlecloud
            service_account_json "{\"type\":\"service_account\", ...}"
            project_id "my-gcp-project"
            managed_zone "example-com"
        }
    }
}

OVH⁠#

Provider name: ovh

DirectiveArgumentsDescriptionDefault
application_key<string>OVH application key.none (required)
application_secret<string>OVH application secret.none (required)
consumer_key<string>OVH consumer key.none (required)
endpointovh-eu, ovh-ca, kimsufi-eu, kimsufi-ca, soyoustart-eu, soyoustart-caOVH API endpoint region.none (required)

Configuration example:

*.example.com {
    tls {
        provider acme
        challenge dns-01

        dns {
            provider ovh
            application_key "YOUR_APP_KEY"
            application_secret "YOUR_APP_SECRET"
            consumer_key "YOUR_CONSUMER_KEY"
            endpoint "ovh-eu"
        }
    }
}

Porkbun⁠#

Provider name: porkbun

DirectiveArgumentsDescriptionDefault
api_key<string>Porkbun API key.none (required)
secret_key<string>Porkbun secret API key.none (required)

Configuration example:

*.example.com {
    tls {
        provider acme
        challenge dns-01

        dns {
            provider porkbun
            api_key "YOUR_PORKBUN_API_KEY"
            secret_key "YOUR_PORKBUN_SECRET_KEY"
        }
    }
}

RFC 2136 (TSIG)⁠#

Provider name: rfc2136

Updates DNS records on any authoritative server that supports dynamic updates (RFC 2136) authenticated with TSIG.

DirectiveArgumentsDescriptionDefault
server<uri>DNS server address as a URI with scheme tcp or udp (for example udp://ns1.example.com:53).none (required)
key_name<string>TSIG key name.none (required)
key_secret<string>TSIG key secret, Base64-encoded.none (required)
key_algorithmHMAC-MD5, GSS, HMAC-SHA1, HMAC-SHA224, HMAC-SHA256, HMAC-SHA256-128, HMAC-SHA384, HMAC-SHA384-192, HMAC-SHA512, HMAC-SHA512-256TSIG algorithm.none (required)

Configuration example:

*.example.com {
    tls {
        provider acme
        challenge dns-01

        dns {
            provider rfc2136
            server "udp://ns1.example.com:53"
            key_name "ferron-acme."
            key_secret "BASE64_ENCODED_TSIG_SECRET"
            key_algorithm "HMAC-SHA256"
        }
    }
}

Route 53⁠#

Provider name: route53

DirectiveArgumentsDescriptionDefault
access_key_id<string>AWS access key ID.none (required)
secret_access_key<string>AWS secret access key.none (required)
region<string>AWS region (for example us-east-1).none (optional)
session_token<string>AWS session token for temporary credentials.none (optional)
hosted_zone_id<string>Route 53 hosted zone ID. Ferron resolves the zone automatically if omitted.none (optional)
private_zone_only<bool>Set to true to target a private hosted zone only.false
endpoint<string>Route 53 endpoint URL.none (optional)

Configuration example:

*.example.com {
    tls {
        provider acme
        challenge dns-01

        dns {
            provider route53
            access_key_id "AKIAIOSFODNN7EXAMPLE"
            secret_access_key "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
            region "us-east-1"
            hosted_zone_id "Z1D633PJN98FT9"
        }
    }
}

Spaceship⁠#

Provider name: spaceship

DirectiveArgumentsDescriptionDefault
api_key<string>Spaceship API key.none (required)
api_secret<string>Spaceship API secret.none (required)

Configuration example:

*.example.com {
    tls {
        provider acme
        challenge dns-01

        dns {
            provider spaceship
            api_key "YOUR_SPACESHIP_API_KEY"
            api_secret "YOUR_SPACESHIP_API_SECRET"
        }
    }
}

Command (external hook)⁠#

Provider name: command

The command provider runs an external program for every DNS record change. It does not call any DNS API directly. Use it to delegate updates to any DNS server or automation that Ferron does not support natively. The program receives the record details through environment variables and must exit with status 0 to signal success.

DirectiveArgumentsDescriptionDefault
command<string>Absolute path to the program to run for each record change.none (required)
min_ttl<int>Minimum TTL (in seconds) that Ferron will accept for records created by this provider.60

The provider passes the following environment variables to the program:

VariableSet onDescription
FERRON_DNS_ACTIONalwaysadd for update_record, delete for delete_record.
FERRON_DNS_DOMAINalwaysFull record name (for example _acme-challenge.example.com).
FERRON_DNS_RECORD_TYPEalwaysRecord type (for example TXT).
FERRON_DNS_RECORD_VALUEadd onlyThe record value (the ACME DNS challenge token).
FERRON_DNS_RECORD_TTLadd onlyThe requested TTL in seconds.

The program runs directly (no shell), so arguments beyond the program path are not supported. Pass dynamic data through the environment variables. The program’s exit status determines success: 0 means the change is applied, any other status is treated as an error.

Note

After the program creates the _acme-challenge TXT record, propagation time depends entirely on your script and DNS server. Ferron cannot know the delay, so verify the record is publicly resolvable before issuance retries, or set a short TTL.

Configuration example:

*.example.com {
    tls {
        provider acme
        challenge dns-01

        dns {
            provider command
            command "/usr/local/bin/ferron-dns-hook.sh"
            min_ttl 30
        }
    }
}

A minimal hook script:

#!/bin/sh
# Example hook: log the requested change. Replace with your real DNS update.
echo "$(date) $FERRON_DNS_ACTION $FERRON_DNS_RECORD_TYPE $FERRON_DNS_DOMAIN $FERRON_DNS_RECORD_VALUE" >> /var/log/ferron-dns.log
exit 0

Usage notes⁠#

Using environment variables for credentials⁠#

All string directives support environment variable interpolation. This keeps secrets out of your configuration file:

*.example.com {
    tls {
        provider acme
        challenge dns-01

        dns {
            provider cloudflare
            api_key "{{env.CF_API_TOKEN}}"
        }
    }
}

DNS propagation delays⁠#

After Ferron creates the _acme-challenge TXT record, the ACME CA must be able to resolve it. Propagation time varies by provider:

ProviderTypical minimum TTL
rfc2136, route531 s
bunny15 s
digitalocean30 s
cloudflare, dnsimple, googlecloud, ovh, spaceship60 s (1 min)
porkbun600 s (10 min)
desec3600 s (1 h)

If certificate issuance fails with a DNS validation error, verify that the TXT record is resolvable from the public internet before retrying.

RFC 2136 TSIG key format⁠#

The key_secret value must be the raw TSIG key bytes encoded as standard Base64 (with padding). Most DNS management tools (BIND tsig-keygen, dnssec-keygen) output the key in this format already.

OVH endpoint selection⁠#

Choose the endpoint that matches where your domain resides:

ValueRegion
ovh-euOVH Europe
ovh-caOVH North America / Canada
kimsufi-euKimsufi Europe
kimsufi-caKimsufi North America
soyoustart-euSo you Start Europe
soyoustart-caSo you Start North America

See also⁠#

Best practices⁠#

ferron doctor reports the following best-practice check for DNS provider directives.

  • Secrets in plain configuration. DNS provider credentials (api_key, secret, token, and so on) should use environment variable interpolation ({{env.VAR}}) rather than plain strings. This avoids leaking secrets in version control or logs.