Chief Tools Logo
Guides

Issue certificates with DNS-01 validation

ACME certificate authorities such as Let's Encrypt can verify that you control a domain by asking you to publish a TXT record on its _acme-challenge name. This is the DNS-01 challenge. It is the only challenge type that can issue wildcard certificates, and it works for servers that are not reachable from the internet.

Domain Chief has two endpoints for this: one publishes a challenge value and one deletes it. A token with the domainchief:acme:write scope can call them and nothing else, so a server that renews certificates never holds a token that can change your other DNS records.

Use an existing client when one fits:

Before you start

Domain requirements

The domain must be in the team that owns the token and must use Domain Chief hosted DNS. The challenge records are published in the hosted DNS zone, so a domain that uses other nameservers cannot be validated this way.

Create a token

Create a team access token with only the domainchief:acme:write scope. Select the team that owns the domains you issue certificates for.

A team access token keeps working when the person who created it leaves the team, and it never switches to another team. See authentication for the token types and scopes for what the scope allows.

Anyone holding the token can obtain certificates for every domain in the team. Store it like any other credential and give each server or cluster its own token, so you can revoke one without affecting the others.

Use an existing client

Certbot

The certbot-dns-domainchief plugin adds a dns-domainchief authenticator to Certbot. Install it the same way you installed Certbot:

TerminalCode
# Certbot installed as a snap snap install certbot-dns-domainchief snap set certbot trust-plugin-with-root=ok snap connect certbot:plugin certbot-dns-domainchief # Certbot installed with pip pip install certbot-dns-domainchief

Save the token in a credentials file that only the user running Certbot can read:

Code
# ~/.secrets/domainchief.ini dns_domainchief_api_key = $TOKEN

Then request a certificate:

TerminalCode
certbot certonly \ --authenticator dns-domainchief \ --dns-domainchief-credentials ~/.secrets/domainchief.ini \ -d example.com \ -d '*.example.com'

Certbot stores the credentials path and reuses it for renewals. The plugin waits a fixed time for the record to reach the nameservers before Certbot asks for validation. Raise it with --dns-domainchief-propagation-seconds if validation fails because the record was not found.

cert-manager

The cert-manager-webhook-domainchief webhook adds Domain Chief as a DNS01 solver for cert-manager. Install it into the cert-manager namespace:

TerminalCode
helm install cert-manager-webhook-domainchief \ oci://ghcr.io/chieftools/charts/cert-manager-webhook-domainchief \ --namespace cert-manager

Store the token in a Secret the webhook can read:

TerminalCode
kubectl -n cert-manager create secret generic domainchief-api-key --from-literal=api-key=$TOKEN

Reference the webhook from an issuer's DNS01 solver:

YAMLCode
apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt spec: acme: server: https://acme-v02.api.letsencrypt.org/directory privateKeySecretRef: name: letsencrypt-account-key solvers: - dns01: webhook: groupName: acme.domain.chief.app solverName: domainchief config: apiKeySecretRef: name: domainchief-api-key key: api-key

The webhook publishes the challenge on the _acme-challenge name in the domain's own hosted DNS zone, so the cnameStrategy: Follow solver option is not supported.

cert-manager checks the nameservers itself and only asks for validation once the record is visible. The chart documentation lists every chart value, including how to read the Secret from other namespaces for namespaced issuers and how to run multiple replicas.

Publish and delete challenges

Use these requests when you build your own ACME client or add Domain Chief to one. The flow for each challenge is:

  1. Your ACME client receives a DNS-01 challenge and computes the TXT value.
  2. Publish the value on the _acme-challenge name.
  3. Wait until the nameservers serve it.
  4. Tell the certificate authority to validate the challenge.
  5. Delete the value once validation finishes, whether it succeeded or not.

Publish a challenge value

Send the fully qualified challenge name and the value to publish ACME challenge:

Code
POST /api/v1/acme/challenges HTTP/1.1 Host: domain.chief.app Authorization: Bearer $TOKEN Accept: application/json Content-Type: application/json { "name": "_acme-challenge.www.example.com", "value": "gfj9Xq...Rg85nM" }

The name must start with _acme-challenge.. A trailing dot is accepted, so you can pass the name exactly as most ACME libraries return it. The value is the base64url-encoded challenge value, without padding.

You don't send the domain. Domain Chief finds it from the name, using the most specific matching domain in the team. The response returns an ACMEChallenge with the domain it chose:

JSONCode
{ "data": { "id": "$RECORD_ID", "domain": "example.com", "name": "_acme-challenge.www.example.com", "value": "gfj9Xq...Rg85nM", "ttl": 60, "created_at": "2026-10-30T12:20:12.000000Z" } }

Publishing is safe to repeat. If the value is already published, Domain Chief returns the existing challenge instead of adding a second record.

Other values on the same name are kept. A certificate for both example.com and *.example.com needs two values on _acme-challenge.example.com, and you can publish both before validating either.

Wait for the record

A successful response means Domain Chief accepted the value, not that the nameservers already serve it. Publishing to the hosted DNS nameservers takes a short time.

Before you ask the certificate authority to validate, look up the TXT record on the domain's authoritative nameservers until the value appears. Query the authoritative nameservers directly. A public resolver may cache the earlier answer and keep returning it for longer than the record takes to publish.

Delete a challenge value

After validation, delete the value with delete ACME challenge. Pass the same name and value as query parameters:

Code
DELETE /api/v1/acme/challenges?name=_acme-challenge.www.example.com&value=gfj9Xq...Rg85nM HTTP/1.1 Host: domain.chief.app Authorization: Bearer $TOKEN Accept: application/json

Only that value is deleted. Values for other challenges on the same name stay published, so concurrent renewals don't remove each other's records.

Deleting a value that is not published also succeeds, so a cleanup step can run safely after a failed or repeated attempt. If your client crashes before cleaning up, Domain Chief removes challenge values automatically after 24 hours.

Handle errors

Errors use the standard error response. Branch on the HTTP status and, for 409 responses, on the code:

StatusMeaningWhat to do
401The token is missing, expired, or revoked.Replace the token.
403The token has neither domainchief:acme:write nor domainchief:dns:write.Create a token with domainchief:acme:write.
404No domain in the team matches the challenge name.Check that the domain is in the token's team.
409 with dns.not_hostedThe domain does not use hosted DNS.Retrying does not help until the domain uses hosted DNS.
409 with dns.zone_busy or dns.zone_changedAnother hosted DNS change for the domain was running.Retry after a few seconds.
422The name or value is invalid.Fix the request using the messages in errors.
5xxDomain Chief could not complete the request.Retry with a growing delay. Publishing and deleting are safe to repeat.

Treat unknown code values as permanent failures and report the message to the person running the client.