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:
- an explicit path passed to the agent,
- the
TOKENTIMER_AGENT_CONFIG_DIRenvironment variable, - the OS default.
Restart the agent after editing: sudo systemctl restart tokentimer-agent.
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
| Field | Type | Default | Description |
|---|---|---|---|
serverUrl | string | required | Control-plane base URL. Must be https: unless allowInsecureLocalHttp is set and the host is loopback. |
agentId | string | null | null | Set by the agent after registration. Do not hand-edit. |
protocolVersion | string | current | Agent protocol version advertised at registration. |
heartbeatIntervalMs | integer | 30000 | Heartbeat frequency. Also how quickly the agent notices retirement and signing-key rotation. |
pollIntervalMs | integer | 15000 | How often the agent polls for claimable jobs. |
caBundlePath | string | null | null | PEM CA bundle used to trust the control plane, for a private CA or a TLS-inspecting egress proxy. Overridden by TOKENTIMER_AGENT_CA_BUNDLE. |
allowInsecureLocalHttp | boolean | false | Permits 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:
| Field | Type | Default | Description |
|---|---|---|---|
declaredTargetSelectors | string[] | [] | Targets (hosts, domains) this agent handles. |
declaredCommandProfileNames | string[] | [] | 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.
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
| Field | Type | Default | Description |
|---|---|---|---|
execution.enabled | boolean | false | Master switch for doing real work. While false the agent still registers, heartbeats, and reports. |
execution.dryRun | boolean | true | Plan and report the steps a job would run, with no filesystem or process side effects. |
execution.keysDir | string | <configDir>/keys | Where locally generated private keys are written, mode 0600. Keys never leave the host. |
execution.replayStorePath | string | <configDir>/replay-store.json | Persisted nonce cache that makes replayed jobs unexecutable across restarts. |
execution.outboxDir | string | <configDir>/outbox | Durable queue for results that could not be delivered yet. |
execution.clockDriftToleranceMs | integer | 30000 | Accepted skew when validating a signed job's validity window. |
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.
| Field | Type | Description |
|---|---|---|
policy.allowedCommands | object | Map 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.allowedPaths | string[] | Absolute paths the agent may write certificates into. |
policy.allowedCaEndpoints | string[] | ACME CA endpoints the agent may talk to, including internal PKI. |
policy.allowedDnsZones | string[] | DNS zones the agent may publish challenge records in. |
policy.allowedDnsProviders | string[] | 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.
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.
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
| Field | Type | Default | Description |
|---|---|---|---|
pinnedSigningKey | object | null | null | { 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
| Field | Type | Default | Description |
|---|---|---|---|
discovery.directories | string[] | none | Directories scanned for existing certificates to report into inventory. |
discovery.intervalMs | integer | 3600000 | Scan interval. |
Discovery is read-only: it reports what it finds and never modifies or moves certificates.
DNS and ACME
| Field | Type | Default | Description |
|---|---|---|---|
dnsProviders | object | null | null | Map of provider id to { credentialsFile } plus the reserved zoneProviderMap. |
dnsPropagation | object | see below | Propagation wait behaviour after publishing and cleanup. |
acmeAccounts | object | null | null | Map 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" }
}
}
Related
- Install and upgrade an agent - installer flags that write parts of this file.
- DNS-01 providers - provider credentials and propagation.
- Automation and executors - how jobs reach the agent.