Skip to main content
Version: 0.11

Agent configuration reference

Where the config lives

The agent reads config.json from its state directory, which the installer creates at /opt/tokentimer-agent/state/config.json with mode 0600, owned by the tokentimer-agent user.

The directory is resolved in this order:

  1. an explicit path passed to the agent,
  2. the TOKENTIMER_AGENT_CONFIG_DIR environment variable,
  3. the OS default.

Restart the agent after editing: sudo systemctl restart tokentimer-agent.

Invalid config fails loudly

Every field is validated at load time and a malformed value aborts startup with a descriptive error naming the field. This is deliberate: a typo in an allowlist must never be silently ignored, because silently ignoring it is what would permit unsafe work.

Connection

FieldTypeDefaultDescription
serverUrlstringrequiredControl-plane base URL. Must be https: unless allowInsecureLocalHttp is set and the host is loopback.
agentIdstring | nullnullSet by the agent after registration. Do not hand-edit.
protocolVersionstringcurrentAgent protocol version advertised at registration.
heartbeatIntervalMsinteger30000Heartbeat frequency. Also how quickly the agent notices retirement and signing-key rotation.
pollIntervalMsinteger15000How often the agent polls for claimable jobs.
caBundlePathstring | nullnullPEM CA bundle used to trust the control plane, for a private CA or a TLS-inspecting egress proxy. Overridden by TOKENTIMER_AGENT_CA_BUNDLE.
allowInsecureLocalHttpbooleanfalsePermits plain http: for loopback hosts only. Development use.

Capability declaration

These tell the control plane what this agent is willing and able to do, and are what job routing matches against:

FieldTypeDefaultDescription
declaredTargetSelectorsstring[][]Targets (hosts, domains) this agent handles.
declaredCommandProfileNamesstring[][]Command profiles this agent offers, for example a certbot or acme.sh profile.

Declaring a capability does not grant it. The local policy block still has to allow the concrete command, path, CA endpoint, and DNS zone.

Read once, at registration

declaredTargetSelectors and declaredCommandProfileNames are sent to the control plane only in the register message, the first time this agent ID enrolls. Editing config.json and restarting the agent does not update what the control plane has on file: heartbeats do not resend these fields. A job that needs a target or command profile added after the fact will not route to this agent until you either re-register it (new credential, new agent ID) or the control plane gains a way to re-declare capabilities on an existing registration. Plan the full target and command-profile list before your first register call in production; see Agents and the execution plane for the audit event that records exactly what was recorded.

Execution

FieldTypeDefaultDescription
execution.enabledbooleanfalseMaster switch for doing real work. While false the agent still registers, heartbeats, and reports.
execution.dryRunbooleantruePlan and report the steps a job would run, with no filesystem or process side effects.
execution.keysDirstring<configDir>/keysWhere locally generated private keys are written, mode 0600. Keys never leave the host.
execution.replayStorePathstring<configDir>/replay-store.jsonPersisted nonce cache that makes replayed jobs unexecutable across restarts.
execution.outboxDirstring<configDir>/outboxDurable queue for results that could not be delivered yet.
execution.clockDriftToleranceMsinteger30000Accepted skew when validating a signed job's validity window.
Both switches must be flipped to execute

A fresh install has enabled: false and dryRun: true. Setting enabled: true alone still yields dry-run planning. Real execution requires enabled: true and dryRun: false. Validate a rollout in dry-run first; it exercises the entire trust chain (claim, signature verification, replay check, policy evaluation) without touching the host.

Policy (default-deny)

The policy block is the agent's own authorization boundary. It always wins over control-plane intent: a job that exceeds it is rejected locally, and the rejection is reported back as evidence.

FieldTypeDescription
policy.allowedCommandsobjectMap of profile name to { argv: [...] }. argv is the allowlisted executable plus any fixed leading flags you want to require, not the whole command line: for the built-in certbot/acme.sh CSR flows the agent appends the subcommand and its own flags (certonly/--signcsr, --csr <path>, the DNS hook flags, --server <caEndpoint>, output paths) itself, so repeating them here duplicates them and the tool fails to parse.
policy.allowedPathsstring[]Absolute paths the agent may write certificates into.
policy.allowedCaEndpointsstring[]ACME CA endpoints the agent may talk to, including internal PKI.
policy.allowedDnsZonesstring[]DNS zones the agent may publish challenge records in.
policy.allowedDnsProvidersstring[]DNS provider ids, exact-match. See DNS-01 providers.
{
"policy": {
"allowedCommands": {
"certbot-csr": { "argv": ["/usr/bin/certbot"] }
},
"allowedPaths": ["/etc/nginx/certs"],
"allowedCaEndpoints": ["https://acme-v02.api.letsencrypt.org/directory"],
"allowedDnsZones": ["example.com"],
"allowedDnsProviders": ["cloudflare"]
}
}

The argv template is only the allowlisted executable (and any fixed leading flags you want to require). For the built-in certbot/acme.sh CSR adapters, the agent itself appends certonly/--signcsr, --csr <path>, the DNS hook flags, the CA endpoint, and the cert output path. Do not repeat those in argv, doing so duplicates the flags and certbot/acme.sh will fail to parse the command line.

acme.sh must not live under a home directory

The hardened unit runs with ProtectHome=true, which makes /home, /root, and /run/user invisible to the process. acme.sh's own installer defaults to ~/.acme.sh, which the agent then cannot execute at all. Install the acme.sh script itself somewhere outside any home directory (for example /opt/acme.sh) and point policy.allowedCommands.acmesh-signcsr.argv at that path. This only affects the executable's own install location; the agent already manages acme.sh's separate --home/--config-home working state under its own state directory regardless of where the executable lives. See the acme.sh setup steps for a full walkthrough.

Key export cannot be enabled

Requests to export private key material are rejected unconditionally. There is no policy field, config flag, or control-plane setting that permits it. That is the zero-custody invariant, not a default you can relax.

policy.allowedPaths and the systemd ReadWritePaths list are two independent gates, and a deploy needs both. The installer's --write-path manages the systemd side; this field manages the agent side.

Trust pinning

FieldTypeDefaultDescription
pinnedSigningKeyobject | nullnull{ signingKeyId, publicKeyPem } of the control-plane job-signing key the agent trusts.

The agent pins the signing key on first contact and then refuses jobs signed by anything else. It is written by the agent, not by you. During a rotation the agent re-pins automatically after learning about the new key on heartbeat; see Rotate the job-signing key.

Discovery

FieldTypeDefaultDescription
discovery.directoriesstring[]noneDirectories scanned for existing certificates to report into inventory.
discovery.intervalMsinteger3600000Scan interval.

Discovery is read-only: it reports what it finds and never modifies or moves certificates.

DNS and ACME

FieldTypeDefaultDescription
dnsProvidersobject | nullnullMap of provider id to { credentialsFile } plus the reserved zoneProviderMap.
dnsPropagationobjectsee belowPropagation wait behaviour after publishing and cleanup.
acmeAccountsobject | nullnullMap of account reference to { credentialsFile }.

Credentials are always referenced by absolute path to a 0600 file, never inlined into config.json. The agent refuses a credentials file that is group- or other-readable on POSIX hosts.

Full per-provider fields, zone routing, and propagation tuning: DNS-01 providers.

Minimal working example

{
"serverUrl": "https://tokentimer.example.com",
"declaredTargetSelectors": ["web01.example.com"],
"declaredCommandProfileNames": ["certbot-csr"],
"execution": {
"enabled": true,
"dryRun": false
},
"policy": {
"allowedCommands": {
"certbot-csr": { "argv": ["/usr/bin/certbot"] }
},
"allowedPaths": ["/etc/nginx/certs"],
"allowedCaEndpoints": ["https://acme-v02.api.letsencrypt.org/directory"],
"allowedDnsZones": ["example.com"],
"allowedDnsProviders": ["cloudflare"]
},
"dnsProviders": {
"cloudflare": { "credentialsFile": "/etc/tokentimer-agent/dns/cloudflare.json" }
}
}