Troubleshoot local policy and verification failures
Goal
Diagnose a job that fails or is rejected on the agent host even though everything upstream looked fine: it was created, matched to an online agent, signed, and dispatched. This covers the two places that failure most often comes from - the agent's own local policy boundary, and the verify step it runs after a reload.
Why an agent can reject a job the control plane already accepted
Routing a job to an agent and authorizing what that agent may do are two separate decisions. The control plane matches a job to an agent by declared target selectors, DNS providers, and command profiles, then signs and dispatches it. What the agent actually does with that job is governed entirely by its own policy block in config.json, on the host, which the control plane cannot see into and cannot override. See Policy (default-deny) for the full field reference; this page is about what happens when a job hits that boundary.
This is deliberate, not a bug: it is the agent's authorization boundary, and it is meant to win even when the control plane's own matching was permissive. A job that exceeds it is rejected locally and reported back as evidence, never silently dropped and never executed anyway.
rejectionReason | What it means | Where to fix it |
|---|---|---|
command_not_allowlisted | The job's commandRef does not match any profile in policy.allowedCommands | Add the profile to policy.allowedCommands |
path_not_allowlisted | certPath or chainPath is outside policy.allowedPaths | Add the path to policy.allowedPaths, and confirm the systemd sandbox and filesystem permissions also allow it - see the --write-path warnings in the install runbook, since this is a separate gate from policy |
ca_endpoint_not_allowlisted | caEndpoint is not in policy.allowedCaEndpoints | Add the endpoint to policy.allowedCaEndpoints |
dns_zone_not_allowlisted | dnsZone is not covered by policy.allowedDnsZones | Add the zone to policy.allowedDnsZones |
dns_provider_not_allowlisted | dnsProvider is not in policy.allowedDnsProviders | Add the provider id to policy.allowedDnsProviders; see DNS-01 providers for valid ids |
target_out_of_scope | The job's target, or a verifyHost, is outside what this agent is authorized to touch | See The verify step below if it is a verify destination; otherwise check declaredTargetSelectors |
key_export_requested | The job asked the agent to export private key material | Not fixable. Key export is rejected unconditionally - there is no policy setting that permits it |
policy.allowedCommands (and the other policy.* allowlists) are read fresh from config.json on every job, so editing the file and restarting the agent is enough to widen them. declaredCommandProfileNames and declaredTargetSelectors are different: they are sent to the control plane only once, at registration, and a config edit does not update what the control plane has on file. If a job never routes to an agent at all (rather than routing and then being rejected), you likely need to fix the declared side, not the policy side - see Capability declaration.
Step 1 - Find the exact rejection reason
Open the job on the Jobs tab of Certificate operations. A locally-rejected job shows status rejected, and its timeline carries a policy-evidence entry with the rejectionReason from the table above plus a free-text detail string naming exactly what did not match (for example, the specific commandRef or path the agent compared against its allowlist).
If you have host access, the agent's own log names the same reason at the moment of rejection:
journalctl -u tokentimer-agent -n 100 --no-pager | grep -i rejected
Step 2 - Widen the policy, then re-run
- Edit
/opt/tokentimer-agent/state/config.jsonand add the missing value to the relevantpolicy.*list. - Restart the agent:
sudo systemctl restart tokentimer-agent. - Re-run the job (or wait for the next scheduled attempt, if it still has attempts left).
No new bootstrap token or re-registration is needed for a policy.* change - only declaredCommandProfileNames/declaredTargetSelectors require that, per the tip above.
The verify step
After a successful deploy and reload, the agent can optionally open a TLS connection to the live endpoint and confirm it is actually serving the certificate it just deployed, by comparing the SHA-256 fingerprint. This only runs when the job payload sets verifyHost (optionally with verifyPort); a job with no verifyHost skips it entirely and relies on the deploy-time fingerprint read-back alone.
Configuring allowedVerifyHosts
Verifying the domain the job is actually renewing needs no extra configuration - the agent always allows a verify destination that matches the job's own certificate target. You only need policy.allowedVerifyHosts when verifyHost points somewhere else: a load balancer VIP in front of several backends, an internal alias, or localhost/127.0.0.1 while you are validating a new agent by hand.
Two destination classes behave specially, and no configuration can change either:
- Loopback (
127.0.0.1,::1,localhost) is allowed only with an explicit entry inpolicy.allowedVerifyHosts, even when it happens to match the job's target. - Link-local and metadata-endpoint-class addresses (including
169.254.169.254) are hard-denied. They can never be probed, allowlisted or not.
A hostname entry in policy.allowedVerifyHosts also covers its subdomains at a dot boundary; an IP entry only ever matches exactly.
{
"policy": {
"allowedVerifyHosts": ["localhost", "127.0.0.1", "lb.internal.example.com"]
}
}
"Verify step failed: live endpoint does not serve the deployed certificate"
Two different causes produce this message, and they need different fixes:
- Policy rejection. If the job's evidence shows
rejectionReason: target_out_of_scopefor the verify step, this is local policy -verifyHostis not covered bypolicy.allowedVerifyHostsand does not match the job's own target. Fix it as in ConfiguringallowedVerifyHostsabove. - A genuine mismatch was read. The agent actually connected and the certificate it saw did not match. This can be a real deployment problem, but immediately after a reload it can also be transient: a reload can be accepted by the service manager before every worker process has finished switching to the new certificate. If you see this only in the first moment after a reload, and a manual check a few seconds later shows the correct certificate, treat it as transient and re-run the job. If it persists or recurs on every renewal for the same host, treat it as a real deployment problem:
# Compare what the agent should have deployed against what the endpoint actually serves
openssl x509 -in /path/to/deployed/fullchain.pem -noout -fingerprint -sha256
openssl s_client -connect <verifyHost>:<verifyPort> -servername <verifyHost> </dev/null 2>/dev/null \
| openssl x509 -noout -fingerprint -sha256
If the two fingerprints genuinely differ and stay different on a retry, check that the reload target you configured (reloadService, or the reloadCommandRefs on a manual job) is the same process actually serving verifyHost - a reverse proxy or a second listener not covered by the reload is the most common real cause.
Related
- Agent configuration reference - the full
policyfield list. - Install and upgrade an agent - installer-level and systemd-sandbox troubleshooting.
- Automation and executors - how jobs are planned, routed, and dispatched.
- Reconciling interrupted jobs - when a job stops reporting entirely, rather than being rejected outright.