DNSCove

DNSCove guide

Free TLS certificates with Let's Encrypt (any server)

You don't need Kubernetes to get free, auto-renewing TLS from Let's Encrypt on a domain served by DNSCove — including wildcard certificates. Any ACME client that supports a DNS-01 hook (certbot, acme.sh, lego, …) can solve the challenge through one DNSCove endpoint, authenticated by a scoped acme-dns01 token.

That token is the whole point: it can do exactly one thing — add and remove values on the _acme-challenge TXT record of a single zone. It can't read your other records, can't touch your A/MX/NS, and can't reach any other zone. So the credential that lives on your web server, in a cron job, next to your private keys, is one that can't hurt you if it leaks. (Contrast a Route 53 DNS-01 setup, where the IAM policy that lets your client write the challenge — route53:ChangeResourceRecordSets — can rewrite every record in the hosted zone.)

your ACME client ──(DNS-01 hook)──▶ POST /api/zones/{zoneId}/acme-challenge
                                       Authorization: Bearer <acme-dns01 token>
                                       { action: PRESENT, name, value }
                                       │
     control plane merges the _acme-challenge TXT set, publishes to the edge,
     and only returns 200 once the record is live on ns1/ns2  ◀── no propagation guessing
                                       │
     Let's Encrypt resolves _acme-challenge.<domain> and issues the cert ✓

On Kubernetes? Use the cert-manager guide instead — it wraps this same endpoint in a DNS-01 solver webhook.

Before you start

1. Mint a scoped acme-dns01 token

Create the narrowest token there is, bound to your zone. From the console: open the zone → API tokens → new token → scope acme-dns01. Or over the API with a session / tenant-admin token:

curl -fsS -X POST https://api.dnscove.com/api/zones/<ZONE_ID>/tokens \
  -H "Authorization: Bearer <SESSION_OR_TENANT_TOKEN>" \
  -d '{"name":"letsencrypt on web-01","scope":"acme-dns01"}' | jq -r .token
# dnsc_… — copy it now; it isn't shown again.

Keep it on the server as an environment variable alongside the zone id:

export DNSCOVE_API_BASE=https://api.dnscove.com
export DNSCOVE_ZONE_ID=Z1a2b3c4d5e6f70
export DNSCOVE_ACME_TOKEN=dnsc_…

2. The challenge endpoint

Every client path below calls the same endpoint. It takes a tiny JSON body:

POST {DNSCOVE_API_BASE}/api/zones/{DNSCOVE_ZONE_ID}/acme-challenge
Authorization: Bearer {DNSCOVE_ACME_TOKEN}
Field Value
action PRESENT to add the challenge value, CLEANUP to remove it
name The challenge record, e.g. _acme-challenge.example.com
value The key-authorization digest your ACME client computed (base64url)

Two properties make this pleasant to script:

Path A — certbot

certbot drives DNS-01 through two hook scripts. Save this as /usr/local/bin/dnscove-hook.sh and chmod +x it — it handles both the present and cleanup calls:

#!/usr/bin/env sh
# certbot manual DNS-01 hook for DNSCove.
# Used for both --manual-auth-hook (PRESENT) and --manual-cleanup-hook (CLEANUP).
set -eu

: "${DNSCOVE_API_BASE:=https://api.dnscove.com}"
: "${DNSCOVE_ZONE_ID:?set DNSCOVE_ZONE_ID}"
: "${DNSCOVE_ACME_TOKEN:?set DNSCOVE_ACME_TOKEN}"

# certbot sets CERTBOT_AUTH_OUTPUT only on the cleanup hook.
if [ -n "${CERTBOT_AUTH_OUTPUT+x}" ]; then action=CLEANUP; else action=PRESENT; fi

curl -fsS -X POST \
  "${DNSCOVE_API_BASE}/api/zones/${DNSCOVE_ZONE_ID}/acme-challenge" \
  -H "Authorization: Bearer ${DNSCOVE_ACME_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d "{\"action\":\"${action}\",\"name\":\"_acme-challenge.${CERTBOT_DOMAIN}\",\"value\":\"${CERTBOT_VALIDATION}\"}" \
  >/dev/null

Issue the certificate — wildcards included:

certbot certonly \
  --manual --preferred-challenges dns \
  --manual-auth-hook /usr/local/bin/dnscove-hook.sh \
  --manual-cleanup-hook /usr/local/bin/dnscove-hook.sh \
  -d example.com -d '*.example.com'

certbot records the hook paths in the renewal config, so unattended renewal just works:

certbot renew --dry-run   # exercise it once

Keep DNSCOVE_ZONE_ID and DNSCOVE_ACME_TOKEN available to the renewal environment (e.g. exported in the systemd timer / cron that runs certbot renew, or written into the hook script itself). The hook can't call the API without them.

Path B — acme.sh

acme.sh persists your credentials and installs its own renewal cron, so it's the lightest way to keep certs fresh on a plain host. Drop this minimal DNS API plugin at ~/.acme.sh/dnsapi/dns_dnscove.sh:

#!/usr/bin/env sh
# DNSCove DNS-01 plugin for acme.sh.  Usage:
#   export DNSCOVE_ZONE_ID=Z…  DNSCOVE_ACME_TOKEN=dnsc_…
#   acme.sh --issue --dns dns_dnscove -d example.com -d '*.example.com'

dns_dnscove_add() {
  _dnscove_req PRESENT "$1" "$2"
}

dns_dnscove_rm() {
  _dnscove_req CLEANUP "$1" "$2"
}

_dnscove_req() {
  _action="$1"; _fulldomain="$2"; _txtvalue="$3"

  DNSCOVE_API_BASE="${DNSCOVE_API_BASE:-$(_readaccountconf_mutable DNSCOVE_API_BASE)}"
  DNSCOVE_API_BASE="${DNSCOVE_API_BASE:-https://api.dnscove.com}"
  DNSCOVE_ZONE_ID="${DNSCOVE_ZONE_ID:-$(_readaccountconf_mutable DNSCOVE_ZONE_ID)}"
  DNSCOVE_ACME_TOKEN="${DNSCOVE_ACME_TOKEN:-$(_readaccountconf_mutable DNSCOVE_ACME_TOKEN)}"

  if [ -z "$DNSCOVE_ZONE_ID" ] || [ -z "$DNSCOVE_ACME_TOKEN" ]; then
    _err "DNSCOVE_ZONE_ID and DNSCOVE_ACME_TOKEN must be set for the first issue."
    return 1
  fi
  # Persist for unattended renewal (acme.sh re-reads these from its account conf).
  _saveaccountconf_mutable DNSCOVE_API_BASE "$DNSCOVE_API_BASE"
  _saveaccountconf_mutable DNSCOVE_ZONE_ID "$DNSCOVE_ZONE_ID"
  _saveaccountconf_mutable DNSCOVE_ACME_TOKEN "$DNSCOVE_ACME_TOKEN"

  export _H1="Authorization: Bearer $DNSCOVE_ACME_TOKEN"
  export _H2="Content-Type: application/json"
  _body="{\"action\":\"$_action\",\"name\":\"$_fulldomain\",\"value\":\"$_txtvalue\"}"
  _url="$DNSCOVE_API_BASE/api/zones/$DNSCOVE_ZONE_ID/acme-challenge"

  if ! _post "$_body" "$_url" "" "POST"; then
    _err "DNSCove $_action request failed"
    return 1
  fi
  _debug2 response "$response"
  return 0
}

Then:

export DNSCOVE_ZONE_ID=Z1a2b3c4d5e6f70
export DNSCOVE_ACME_TOKEN=dnsc_…
acme.sh --issue --dns dns_dnscove -d example.com -d '*.example.com'

acme.sh saves the two variables into its account config, installs a daily renewal cron, and re-reads them at renewal time — no further setup.

Path C — lego and other clients

Any client with a "run a script" DNS provider works the same way. With lego, use the exec provider pointed at a small script that reads lego's EXEC_* environment and issues the same PRESENT / CLEANUP calls from step 2. The API reference documents POST /api/zones/{id}/acme-challenge in full if you're wiring up a client not listed here.

Start on staging. For your first issue, point the client at the Let's Encrypt staging directory (https://acme-staging-v02.api.letsencrypt.org/directory; certbot --test-cert, acme.sh --staging). Staging's rate limits are generous — switch to production once a cert issues cleanly.

Troubleshooting

Symptom Likely cause
Validation times out / NXDOMAIN for _acme-challenge The zone isn't ACTIVE and delegated to ns1/ns2 yet, so Let's Encrypt can't resolve the challenge. Confirm the zone serves (dig _acme-challenge.example.com @ns1.dnscove.com).
403 from the endpoint Token scope or zone binding is wrong. It must be an acme-dns01 (or zone-admin) token for this zoneId.
400 name must be an _acme-challenge record The name you sent doesn't start with _acme-challenge. — pass the challenge FQDN, not the bare domain.
502 PublishPending The record saved but hadn't propagated to every edge when the call returned. It's transient — the client's retry (or a re-run) succeeds.
Renewal fails months later The acme-dns01 token or its env vars went missing from the renewal environment. Re-mint the token and update the cron/hook.

Notes