Worked example: Cloudflare DNS-01
The video uses a private test CA. This written example uses Cloudflare and Let's Encrypt staging.
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
- A Cloud plan that includes CLM agents, and workspace permissions to create jobs.
- 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 registration where possible. On agents 0.18.0 or later, command-profile declarations are refreshed on heartbeat, including removals, and a non-empty target-selector list replaces the stored list. An empty selector list does not clear stored selectors. Older agents may require registration-time declarations. Preserve the registered identity, restart after editing, and confirm a fresh heartbeat. 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.ch",
"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",
"/opt/tokentimer-agent/state/keys"
],
"allowedCaEndpoints": [
"https://acme-staging-v02.api.letsencrypt.org/directory"
],
"allowedDnsZones": [
"example.com"
],
"allowedDnsProviders": [
"cloudflare"
]
},
"dnsProviders": {
"cloudflare": {
"credentialsFile": "/etc/tokentimer-agent/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. Deploys also read the private key from the agent's keys folder, so /opt/tokentimer-agent/state/keys must be in allowedPaths next to the certificate folder; with only the certificate folder the deploy is rejected. 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 bash ./scripts/install-agent.sh \
--api-url https://tokentimer.ch \
--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 0755 /etc/tokentimer-agent
sudo install -m 0600 -o tokentimer-agent -g tokentimer-agent /dev/null \
/etc/tokentimer-agent/cloudflare.json
sudoedit /etc/tokentimer-agent/cloudflare.json
Save the scoped token in this file:
{ "apiToken": "YOUR_SCOPED_CLOUDFLARE_TOKEN" }
First complete the signed no-op smoke check with execution.enabled: true and local execution.dryRun: true. This checks signed dispatch/reporting; it does not validate a later issuance's DNS, paths or reload policy and does not issue, deploy or reload. Then edit the existing config.json to set execution.dryRun to false, preserving the identity and other fields. Restart before real issuance:
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
- Open the job and confirm it reached
succeeded; inspect its timeline and verify evidence. - On Certificates, confirm the managed certificate is
activeand its fingerprint and expiry are populated. - 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 issuance and confirm its profile is declared by the agent. 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.
For a service deployment, configure its certificate paths and an approved configuration-test/reload procedure, then verify the live certificate. The file-only job above does not configure NGINX or prove what a listener serves.
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. For agents 0.18.0 or later, restart after the local edit and confirm the next heartbeat updated the declarations. Empty selectors do not clear the stored list; older agents may publish only at registration.
- 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.
Related
- Issue a certificate for the request contract and IIS targets.
- DNS-01 providers for other providers and propagation settings.
- Renew certificates automatically for unattended operation.