Install and upgrade an agent
Goal
Install the native TokenTimer agent on a host that holds certificates, so renewals, deployments, reloads, and verification run automatically under your control.
This page covers the native agent, which does the certificate work for you. If you only want to report on renewals your own scripts already perform, use Connect an external executor instead.
What gets installed
The agent is a zero-dependency Node.js daemon. It is outbound-only: it opens HTTPS connections to your TokenTimer instance and never listens on a port, so it needs no inbound firewall rule.
| Path | Purpose |
|---|---|
/opt/tokentimer-agent/app | The agent package, read-only at runtime |
/opt/tokentimer-agent/state | Config dir, mode 0700, owned by the agent user |
.../state/config.json | Agent configuration, mode 0600 |
.../state/credential | Per-agent credential, written by the agent at registration |
.../state/bootstrap.env | Bootstrap token, deleted automatically after first successful registration |
.../state/acme/ | certbot and acme.sh config, work, and log directories |
/etc/systemd/system/tokentimer-agent.service.d/override.conf | Generated ReadWritePaths allowlist |
/etc/polkit-1/rules.d/50-tokentimer-agent.rules | Optional, only when you allow service reloads |
The systemd unit is hardened (ProtectSystem=strict, NoNewPrivileges=true), and the agent runs as a dedicated tokentimer-agent system user rather than root.
Requirements
| Requirement | Detail |
|---|---|
| OS | Linux with systemd. The installer generates a systemd unit and a polkit rule, so it does not support non-systemd Linux, macOS, or Windows. |
| Runtime | Node.js 22 or later (>=22.0.0 <25.0.0) |
| Privileges | sudo/root to run the installer (creates a system user, unit file, and directories) |
| Network | Outbound HTTPS from the host to your TokenTimer API URL. No inbound access needed. |
| Control plane | CertOps enabled, plus CERTOPS_SIGNING_ENCRYPTION_KEY and CERTOPS_REGISTRATION_ENCRYPTION_KEY set (see Configuration) |
There is no packaged installer for macOS, Windows, or systemd-less Linux. The agent itself is portable Node.js and can be run manually (node /path/to/app/bin/tokentimer-agent.js with TOKENTIMER_AGENT_CONFIG_DIR set), but you are then responsible for the service supervision, sandboxing, and file permissions that the systemd unit normally provides.
Step 1 - Run the installer
Copy the agent package to the host, then run the installer from the package directory. It does not need a token yet: it pauses at a hidden interactive prompt asking for one, so the secret never reaches your shell history or the process list. Create the token in step 2 below and paste it there.
sudo ./scripts/install-agent.sh \
--api-url https://tokentimer.example.com \
--workspace-id <your workspace id>
The installer prompts for the bootstrap token when neither TOKENTIMER_AGENT_BOOTSTRAP_TOKEN nor --bootstrap-token is set. For unattended installs, prefer the environment variable instead of waiting on the prompt:
sudo TOKENTIMER_AGENT_BOOTSTRAP_TOKEN=ttboot_... ./scripts/install-agent.sh \
--api-url https://tokentimer.example.com \
--workspace-id <your workspace id>
--bootstrap-token on shared hostsIt works, but arguments are visible in process listings while the installer runs. The prompt and the environment variable both avoid that exposure.
Run it with --dry-run first to print every action without touching the system:
sudo ./scripts/install-agent.sh --api-url https://tokentimer.example.com \
--workspace-id <id> --dry-run
Useful options
| Option | Use it for |
|---|---|
--write-path PATH | An absolute directory the agent may write certificates into. Repeatable. Never grants all of /etc. |
--write-paths-file FILE | One absolute path per line, merged with --write-path |
--reload-service NAME | Allow systemctl reload for nginx, apache/apache2, httpd, or haproxy via a generated polkit rule |
--ca-bundle PATH | PEM bundle when your control plane uses a private CA |
--allow-insecure-local-http | Development only, and only for loopback hosts |
--uninstall | Stop and remove the service, app dir, unit, drop-in, and polkit rule |
A typical nginx host:
sudo ./scripts/install-agent.sh \
--api-url https://tokentimer.example.com \
--workspace-id <id> \
--write-path /etc/letsencrypt \
--write-path /etc/nginx/certs \
--reload-service nginx
--write-path only lets the systemd sandbox reach a directory. The tokentimer-agent user must still be able to write there. Grant that before enabling deploy jobs, for example:
sudo setfacl -m u:tokentimer-agent:rwx /etc/nginx/certs
Without this, deploys fail with a permission error even though the path is allowlisted.
Polkit is used rather than sudoers deliberately: the unit keeps NoNewPrivileges=true, under which sudo cannot escalate at all.
Step 2 - Create a bootstrap token
- Open Certificate operations at
/certops/operations. - In the Deploy an agent panel, create a bootstrap token.
- Copy it and paste it at the installer's prompt from step 1. It is a single-use
ttboot_token, shown exactly once, and it is consumed at the agent's first registration.
Step 3 - Verify registration
systemctl status tokentimer-agent
journalctl -u tokentimer-agent -f
You should see the agent register and begin heartbeating, and the Agent fleet panel on the Certificate operations page flip it to registered. The fleet panel shows status, last heartbeat, clock drift, and version compatibility.
Confirm the bootstrap token file is gone once registration succeeded:
sudo ls -l /opt/tokentimer-agent/state/
bootstrap.env should no longer be present; the agent removes it after the first successful registration and keeps only the per-agent credential. Removal is best-effort, so if the file is still there (for example because of a read-only or oddly-permissioned state dir), delete it yourself once the agent is registered.
Step 4 - Start in dry-run, then enable execution
The agent ships with execution.dryRun set to true and execution.enabled set to false, so a fresh install exercises the whole trust chain (claim, signature verification, policy evaluation, planning) and reports what it would do, with no filesystem or process side effects.
Leave it that way until you have seen a job plan correctly, then enable real execution in config.json:
{
"execution": {
"enabled": true,
"dryRun": false
}
}
Restart the agent after editing: sudo systemctl restart tokentimer-agent.
Upgrading
The agent does not self-update, which is deliberate: an execution-plane process that can update itself is an execution-plane process an attacker can update.
- Copy the new package to the host.
- Re-run
install-agent.shwith the same flags. The state directory, credential, and keys are preserved, so the agent keeps its identity and does not need a new bootstrap token. - Confirm the new version in the Agent fleet panel.
To force a fleet upgrade, raise CERTOPS_AGENT_MIN_AGENT_VERSION on the control plane; older agents are then blocked at registration and heartbeat with CERTOPS_AGENT_COMPATIBILITY_BLOCKED rather than silently continuing to claim work.
Retiring and uninstalling
Retire the agent from the Agent fleet panel first. A retired agent exits cleanly on its next heartbeat instead of being killed mid-job. Then, on the host:
sudo ./scripts/install-agent.sh --uninstall
This removes the service, app directory, unit, drop-in, and polkit rule. The state directory and system user are preserved so you cannot destroy keys by accident. Remove them deliberately once you are sure:
sudo rm -rf /opt/tokentimer-agent
sudo userdel tokentimer-agent
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Installer refuses the API URL | URL is not https: | Use HTTPS, or --allow-insecure-local-http for a loopback dev host only |
| Registration fails as unauthorized | Bootstrap token already used, expired, or revoked | Tokens are single-use; create a new one in the dashboard |
| Registration fails and CertOps routes 404 | CertOps disabled on the instance | Enable it on API and worker (see Enable CertOps) |
| Registration fails referencing an encryption key | CERTOPS_REGISTRATION_ENCRYPTION_KEY unset or malformed | Set 64 hex characters and restart the API |
| Agent blocked at registration or heartbeat | Version outside the accepted window | Upgrade the agent, or widen the version range on the control plane |
| TLS handshake failure to the control plane | Private CA not trusted by the agent | Reinstall with --ca-bundle, or set caBundlePath in config.json |
| Deploy fails with a permission error | Path allowlisted but not writable by the agent user | Grant write access (setfacl) to tokentimer-agent |
| Reload fails | Service not allowlisted at install time | Reinstall with --reload-service NAME |
| Clock drift warnings in the fleet panel | Host clock skew | Fix NTP; signed jobs enforce a validity window |
| Jobs plan but never change anything | Still in dry-run | Set execution.enabled: true and execution.dryRun: false, then restart |
Related
- DNS-01 providers - credentials and zone routing for DNS-01 challenges.
- Automation and executors - how jobs are planned, routed, and dispatched.
- Reconciling interrupted jobs - when an agent stops reporting midway.
- Configuration reference - control-plane variables that affect the fleet.