Skip to main content
Version: 0.17

Worked example: Cloudflare DNS-01

Goal​

Issue one certificate for agent-test.example.com through a Linux agent using Cloudflare DNS-01. You will verify both the deployed file and the managed certificate in TokenTimer.

This uses Let's Encrypt staging. Its certificates are not browser-trusted. Use a test hostname and separate output paths while proving the workflow. For HTTP-01 instead, see ACME challenge selection and the webroot example.

Prerequisites​

  • CertOps enabled on the API and its worker. See Enable CertOps.
  • A fresh Linux agent host with systemd, Node.js 22 or 24, and Certbot at /usr/bin/certbot. Obtain the agent package through Install an agent (Linux). See that guide for other host setups and WSL2 testing.
  • A Cloudflare-managed zone and a scoped API token for its DNS records. Use the Edit zone DNS template restricted to your test zone; see Cloudflare credentials.
  • Your workspace ID and the control-plane API URL. Replace WORKSPACE_ID, the hostname, zone, email, and URL in the examples.

Step 1 - Prepare local policy and routing​

Prepare the configuration before the first registration. Target selectors and command profiles are recorded at registration; adding them later does not update the server's record. See Capability declaration.

On the fresh host:

sudo install -d -m 0700 /opt/tokentimer-agent/state
sudoedit /opt/tokentimer-agent/state/config.json

Save this configuration and restrict the file to mode 0600:

{
"serverUrl": "https://tokentimer.example.com",
"workspaceId": "WORKSPACE_ID",
"declaredTargetSelectors": [
"agent-test.example.com"
],
"declaredCommandProfileNames": [
"certbot-csr"
],
"policy": {
"allowedCommands": {
"certbot-csr": {
"argv": [
"/usr/bin/certbot",
"--agree-tos",
"--email",
"ops@example.com"
]
}
},
"allowedPaths": [
"/etc/ssl/tokentimer"
],
"allowedCaEndpoints": [
"https://acme-staging-v02.api.letsencrypt.org/directory"
],
"allowedDnsZones": [
"example.com"
],
"allowedDnsProviders": [
"cloudflare"
]
},
"dnsProviders": {
"cloudflare": {
"credentialsFile": "/etc/tokentimer-agent/dns/cloudflare.json"
}
},
"execution": {
"enabled": false,
"dryRun": true
}
}
sudo chmod 0600 /opt/tokentimer-agent/state/config.json

The installer preserves this file and assigns its ownership to the service user. If an agent is already registered, edit its existing file in place and preserve agentId and signing pins; do not replace it with this fresh-host example.

The DNS provider and zone, ACME command, CA endpoint, and destination directory must all be allowed locally. argv contains the executable and fixed account flags; the agent appends its CSR and DNS challenge flags. Keep execution disabled until credentials are ready.

Step 2 - Install and register the agent​

In Certificate operations → Deploy an agent, create a bootstrap token. Run from the extracted agent package and paste the token at the hidden prompt:

sudo ./scripts/install-agent.sh \
--api-url https://tokentimer.example.com \
--workspace-id WORKSPACE_ID \
--write-path /etc/ssl/tokentimer

Use the same API URL in the installer and serverUrl. It must point to the control-plane API. For a loopback development API, both the installer and config need their explicit insecure-local-HTTP option; see Installer options.

Confirm registration in the dashboard's Agent fleet, then check the host:

systemctl status tokentimer-agent
journalctl -u tokentimer-agent -n 50 --no-pager

Step 3 - Store credentials and enable execution​

The installer has now created the tokentimer-agent user. Create a credentials file it can read:

sudo install -d -m 0700 -o tokentimer-agent -g tokentimer-agent /etc/tokentimer-agent/dns
sudo install -m 0600 -o tokentimer-agent -g tokentimer-agent /dev/null \
/etc/tokentimer-agent/dns/cloudflare.json
sudoedit /etc/tokentimer-agent/dns/cloudflare.json

Save the scoped token in this file:

{ "apiToken": "YOUR_SCOPED_CLOUDFLARE_TOKEN" }

Edit the existing config.json to set execution.enabled to true and execution.dryRun to false, preserving the other fields. Then restart:

sudo systemctl restart tokentimer-agent

Step 4 - Create an issue job​

Open Create manual job in Certificate operations and use this body. API callers must first follow session authentication and CSRF before posting to /api/v1/workspaces/{id}/certops/jobs.

{
"operation": "issue",
"idempotencyKey": "cf-staging-issue-1",
"payload": {
"targetSelector": "agent-test.example.com",
"target": {
"type": "domain",
"reference": "agent-test.example.com"
},
"sans": [
"agent-test.example.com"
],
"commandRef": "certbot-csr",
"caEndpoint": "https://acme-staging-v02.api.letsencrypt.org/directory",
"dnsZone": "example.com",
"dnsProvider": "cloudflare",
"certPath": "/etc/ssl/tokentimer/agent-test.example.com.pem",
"keyPath": "/etc/ssl/tokentimer/agent-test.example.com.key.pem"
}
}

issue creates the managed certificate at provisioning before execution. Do not supply subjectId or certificateId; the server assigns them. certPath and keyPath are concrete files inside the allowlisted destination directory, not directories themselves. Repeating the identical request with the same idempotency key returns the original job.

Step 5 - Verify the result​

  1. Open the job and confirm it reached succeeded; inspect its timeline and verify evidence.
  2. On Certificates, confirm the managed certificate is active and its fingerprint and expiry are populated.
  3. Inspect the deployed certificate on the host:
openssl x509 -in /etc/ssl/tokentimer/agent-test.example.com.pem \
-noout -subject -issuer -dates

The subject should match your test hostname, and the issuer should identify Let's Encrypt staging. A file on disk does not prove a web server is serving it; this example stops at file deployment. Configure a server reload and a TLS verification target when extending it to a served endpoint.

Check renewal, then move to production​

A successful issue normally derives a renewal profile. Check the renewal badge and the Renewals tab; an active certificate alone does not prove automatic renewal is configured. See Renew certificates automatically.

For production, allowlist the production CA endpoint locally and create a new issue request with a new idempotency key and caEndpoint: "https://acme-v02.api.letsencrypt.org/directory". Use the intended production paths. Deployment fields in a derived renewal profile are immutable; changing the agent allowlist alone does not turn the staging profile into a production profile.

Optional: use acme.sh​

Choose the ACME client before first registration. To use acme.sh, install its executable outside a home directory (for example /opt/acme.sh/acme.sh), declare acmesh-signcsr, and allowlist that profile with argv: ["/opt/acme.sh/acme.sh"]. Use commandRef: "acmesh-signcsr" in the job. The installer supplies the DNS hook's working-state directory. See Agent configuration for the systemd home-directory restriction.

Troubleshooting​

  • Registration fails: confirm the API URL, bootstrap token, and trust bundle using Agent installation troubleshooting.
  • Job stays pending: check the registered target selector and command profile. A local edit after registration does not update those declarations.
  • Job reports dry_run_complete: check both execution switches and restart the agent.
  • Policy rejection or permission error: compare the job's command, CA, provider, zone, and file paths with the local allowlists and systemd write grants. See Policy and verification failures.
  • DNS challenge fails: use DNS hook output and troubleshooting. Certbot's detailed log is /opt/tokentimer-agent/state/acme/certbot/logs/letsencrypt.log.
  • Certificate stays provisioning: read Issuance reconciliation reasons. A successful job without sufficient verify evidence cannot prove an active certificate.

Cleanup​

Turn off the test certificate's renewal profile if you no longer need it, then retire the test certificate. Confirm the challenge TXT record was cleaned up. If the agent was created only for this test, retire and uninstall it.