Install and upgrade an agent on Windows
Goal
Install the native TokenTimer agent on a Windows Server host that holds certificates, so renewals, deployments, IIS bindings, and verification run automatically under your control.
This page covers the native Windows Service installer. For the systemd installer, see Install an agent (Linux).
The agent has a native Windows installer and enforces file permissions on win32 with real Windows ACLs, replacing the previous best-effort permission stub that could not guarantee the same protection the systemd path always has. It talks the same protocol to the same TokenTimer control plane as the Linux agent; only the installer and the local permission mechanism differ.
On Windows, the agent can also issue and renew certificates directly into a Windows machine certificate store and bind them to IIS (target.type: "windows-iis"; see the windows-iis target). Private keys for that target are generated and held entirely inside Windows CNG (Cryptography API: Next Generation) and never touch disk as a file. IIS itself, with the site(s) and bindings you intend to manage already present, is a prerequisite for that target only; the agent binds an existing listener to a certificate, it does not create sites.
A domain-joined member that can reach your CA can also enroll from an enterprise AD CS authority. The agent contacts a CA only when a local enrollment rule names it. See Microsoft AD CS.
Requirements
| Requirement | Detail |
|---|---|
| OS | Windows, for the native Windows Service installer. Tested (install, CNG enrollment, real IIS binding, retention, discovery, unattended renewal) on Windows Server 2025, Windows Server 2022, and Windows Server 2019; other Windows Server releases from build 14393 (2016) onward are expected to work the same way but are not independently tested. The certificate store and http.sys binding reads were verified with display languages en-US (Windows Server 2019 and 2022), de-DE, and fr-FR (Windows Server 2025). The agent does not depend on the display language. |
| Runtime | Node.js 22 or later (>=22.0.0 <25.0.0) |
| Windows PowerShell | Windows PowerShell 5.1 (powershell.exe, present by default on Windows Server). The agent reads the machine certificate store and the http.sys bindings through it, also under Constrained Language Mode. Without it, discovery and IIS deploys fail closed. |
| Privileges | An elevated (Administrator) PowerShell session to run the installer (it creates the Windows Service, ACLs, and directories) |
| Network | Outbound HTTPS from the host to your TokenTimer API URL. No inbound access needed. |
| Control plane | CLM enabled, plus CERTOPS_SIGNING_ENCRYPTION_KEY and CERTOPS_REGISTRATION_ENCRYPTION_KEY set (see Configuration) |
Windows PowerShell 5.1 is required for certificate-store and http.sys binding reads. A renewal keeps the binding TLS flags. UNSUPPORTED_BINDING_SETTINGS means the binding has a setting the agent cannot carry over, and the binding is left unchanged. See What happens on renewal.
The permission model, in plain terms
On Linux, the agent's state (its config, credential, generated keys, and job journal) is protected with file modes: 0600 on files, 0700 on the state directory, owned by the tokentimer-agent user. Windows has no direct equivalent of a file mode, so the agent uses icacls to build an equivalent access list on every file and directory it controls, and checks that list on every read:
- Only the agent's own identity and the built-in
SYSTEMaccount are granted access to agent-owned state. Nothing else is. - The built-in
Administratorsgroup is accepted if present (an administrator can always take ownership of any file on the machine, so excluding the group would not add real confidentiality) but the agent never itself grants that group a new permission entry. - Files and directories the agent creates have permission inheritance turned off, so something created later doesn't silently inherit broader access from its parent folder.
- The agent also checks who owns each file, not just who can access it: an untrusted owner could otherwise rewrite the access list at will, which would make the rest of the check meaningless.
This runs on every write, not just at install time, and none of it is best-effort: a missing icacls, a failed icacls call, or a file left with an access list the agent did not expect is treated as a hard failure rather than a warning. That is a deliberate change from the agent's previous Windows behavior, where a failed permission check was silently ignored and the agent continued anyway.
A note on trusting the PowerShell scripts
Windows has no direct equivalent of POSIX's execute bit, so "should I run this script" is a different question here than it is for a downloaded .sh file. install-agent.ps1 itself is verified the same way as the Linux tarball: check its checksum against the release before running it (see Getting the agent package).
The CertOps reference clients go further, since they are meant to be run more casually for debugging. Today, verify them the same way as install-agent.ps1: check the release checksum before running anything. The scripts also run an Authenticode self-check on themselves at startup (with optional -PinnedSignerSubject/-PinnedSignerThumbprint pinning), but that check is defense in depth only, it logs a warning and continues rather than blocking, because tampered code could simply omit the check, so it is never the actual security boundary. If your organization already enforces WDAC (Windows Defender Application Control), an enforced policy validates a signed script at load time before any statement in it executes, which is a real, stronger boundary and works today with no additional configuration of the script itself. A signed pre-execution launcher, which would provide an equivalent boundary on hosts with no WDAC policy deployed, is planned but not yet shipped; until it exists, treat the checksum check and (where deployed) WDAC as the operative controls. See the reference clients page for the concrete self-check flags.
Running the installer
Verify the archive before extracting it. Replace the filename below with the approved version; keep its matching .sha256 file beside it:
$Version = Read-Host 'Approved agent version'
$Archive = "tokentimer-agent-$Version.tgz"
$Expected = ((Get-Content "$Archive.sha256" -Raw).Trim() -split '\s+')[0]
if ($Expected -notmatch '^[a-fA-F0-9]{64}$' -or (Get-FileHash $Archive -Algorithm SHA256).Hash -ine $Expected) {
throw 'Agent archive checksum mismatch'
}
if (Test-Path package) { throw 'Use an empty staging directory; package already exists' }
tar -xzf $Archive
if ($LASTEXITCODE -ne 0) { throw 'Agent extraction failed' }
Set-Location package
$WorkspaceId = Read-Host 'Workspace ID'
Use the installer from that extracted package. Do not substitute a source checkout or copy individual helper binaries into the installed app.
From an elevated (Administrator) PowerShell session, in the directory the agent package was unpacked into:
.\scripts\install-agent.ps1 `
--api-url https://tokentimer.example.com `
--workspace-id "$WorkspaceId" --dry-run
After reviewing the preview, rerun the same command without --dry-run to install.
Like the Linux installer, it pauses at a hidden prompt for the bootstrap token so the secret never touches shell history or a process listing. For unattended installs, inject TOKENTIMER_AGENT_BOOTSTRAP_TOKEN through your secret manager before invoking the command. Do not type a real token into shell history:
.\scripts\install-agent.ps1 --api-url https://tokentimer.example.com --workspace-id "$WorkspaceId"
For a private-CA control plane, add --ca-bundle C:\path\to\api-ca.pem to both preview and installation. The installer copies that public trust bundle into the protected state directory; keep TLS verification enabled.
For a loopback development API over plain http, add --allow-insecure-local-http. The agent then also needs "allowInsecureLocalHttp": true in a hand-written config.json.
install-agent.ps1 still accepts --write-path/--write-paths-file and --reload-service for flag-for-flag parity with the Linux installer. Windows has no OS-level sandbox equivalent to ReadWritePaths= (LocalSystem already has ambient host-wide access), so these flags don't feed a sandbox definition; treat them as a reminder of what to add to config.json's own allowlist yourself.
install-agent.ps1 declares #Requires -Version 5.1 and runs on both Windows PowerShell 5.1 (powershell.exe, present by default on Windows Server) and PowerShell 7+ (pwsh), across Windows Server 2019, 2022, and 2025. Nothing extra needs installing to run the installer itself.
The installer copies the agent package and installs a native Windows Service named TokenTimerAgent, then starts it. The service's binPath does not point at node.exe directly: a plain Node process never calls StartServiceCtrlDispatcher, so the Service Control Manager fails the start and the configured restart policy turns that into a restart loop. Instead, binPath points at a small native service host (no third-party service wrapper: a purpose-built, cross-compiled Go binary shipped inside the agent package) that answers the SCM's start/stop/interrogate protocol on the agent's behalf and launches node.exe plus the agent entry point as its child process. On an upgrade, where the service already exists, it health-checks the restarted service and automatically rolls back to the previous app version, with the credential and configuration preserved, if the health check fails.
The bootstrap token used to create the token (see Step 2 on the Linux page, the flow is identical) is written into that service's registry Environment value only long enough for the process to inherit it as an environment variable; the agent rewrites that value to drop the token as soon as registration succeeds, so it does not linger in the registry after a successful exchange.
| Path | Purpose |
|---|---|
C:\ProgramData\TokenTimerAgent\app | The agent package, read-only at runtime |
C:\ProgramData\TokenTimerAgent\state | Config, credential, and generated keys, ACL-restricted per above |
install-agent.sh's certbot state directories (acme/certbot/{config,work,logs}) are POSIX shell tooling and are not created by install-agent.ps1; certbot itself is not available on Windows. acme.sh's DNS-01 path works: the installer creates the acme/acme.sh/dnsapi/dns_certops.sh hook acme.sh --dns dns_certops needs before its first run. The bundled agent uses DNS-01 for both Linux and Windows targets. For an external HTTP-01 workflow, see ACME challenge selection; it does not use the agent's IIS deployment path.
acme.sh is a shell script, so the agent needs a shell to run it. Install Git for Windows and allowlist C:\Program Files\Git\bin\bash.exe followed by the script path in the command profile's argv.
Verifying registration
Get-Service TokenTimerAgent
You should see Running, and the Agent fleet panel on the Certificate Management page flip the agent to registered. The fleet panel shows status, last heartbeat, clock drift, an Execution column (Enabled or No capability declared), and a Compatibility column (Compatible / Outdated / Blocked).
Before enabling real work, merge execution.enabled: true and execution.dryRun: true into C:\ProgramData\TokenTimerAgent\state\config.json, preserving its existing identity fields, then run Restart-Service TokenTimerAgent.
Save config.json and any DNS credentials file as UTF-8 without a byte order mark. Windows PowerShell 5.1 Set-Content -Encoding utf8 adds one, and the agent then refuses the file. Use Notepad, or:
[IO.File]::WriteAllText($path, $text, (New-Object Text.UTF8Encoding($false)))
A DNS credentials file must be readable only by SYSTEM and Administrators, or the agent refuses it:
icacls $credPath /inheritance:r /grant:r 'SYSTEM:(F)' '*S-1-5-32-544:(F)'
If the DNS challenge is not seen in time, add a dnsPropagation block (see DNS-01 providers).
Using an authenticated API client, list GET /api/v1/workspaces/{workspaceId}/certops/agents. Match items[].agentId to the local configuration’s agentId, then use that row’s items[].id as assignedAgentId; the wire identity and database row UUID are different. With permission to create CertOps jobs, send POST /api/v1/workspaces/{workspaceId}/certops/jobs:
{"operation":"noop","mode":"dry_run","assignedAgentId":"REPLACE_WITH_FLEET_AGENT_ROW_UUID"}
Poll GET /api/v1/workspaces/{workspaceId}/certops/jobs/{jobId}. The expected job.status is dry_run_complete, not succeeded. This checks signed transport; it does not prove certificate issuance or deployment. For AD CS, keep local dry-run enabled during the read-only check, pin and fresh post-pin check. Set execution.dryRun: false and restart only before enrollment or another real mutating operation. For ACME, follow the appropriate issuer runbook. Confirm the actual listener's certificate after the job completes.
Upgrading and uninstalling
For an upgrade, verify and extract the new archive into a separate staging directory, then run its scripts/install-agent.ps1 with the same flags. Do not overwrite files inside the running app directory yourself. The state directory, credential, and keys are preserved, so the agent keeps its identity. The installer still asks for a bootstrap token on every non-dry-run install and checks only its ttboot_ prefix. A registered agent keeps its credential and ignores the token, so paste any ttboot_... value or set $env:TOKENTIMER_AGENT_BOOTSTRAP_TOKEN = 'ttboot_unused'.
Upgrade the control plane before the agents, confirm registration and heartbeats, then upgrade each agent and verify a signed dry run. Use the agent release that matches your Core version. The fleet panel also shows a compatibility result: protocol and version bounds set on the deployment can reject an otherwise matching package.
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.
To uninstall:
.\scripts\install-agent.ps1 --uninstall
This stops and deletes the TokenTimerAgent service and removes the app directory. The state directory is preserved, as on Linux.
Retain that state until the agent is retired and its IIS bindings, certificates and protected-key ownership are accounted for. Uninstalling the service does not revoke certificates or establish that a key is unused. Do not erase custody journals to clear an enrollment error.
Retire the agent from the Agent fleet panel first, the same as described for Linux. Retirement is a control-plane operation and works identically regardless of which platform the agent runs on.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Get-Service TokenTimerAgent shows Stopped | Startup failure, most often registration or config | Check the Windows Event Log (Application) for the service host's output, and config.json for a malformed value |
| 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 and expire 24 hours after creation by default (30 days at most); 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). The disabled state is deliberately indistinguishable from a missing route |
| 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 to the current release, or raise CERTOPS_AGENT_MIN_AGENT_VERSION on the control plane |
| TLS handshake failure to the control plane | The agent does not trust the server certificate (TLS-inspecting egress proxy, or a private CA) | Reinstall with --ca-bundle, or set caBundlePath in config.json |
Installer or agent fails with an icacls error | Missing icacls, or an unexpected owner/access list on an existing state file | Confirm icacls is on PATH. Preserve identity and custody state; do not reset the state directory's ACLs. Have an authorized administrator inspect and repair only the affected file's permissions; see Windows configuration maintenance |
Service stops with is not valid JSON or refuses config.json or a credentials file | The file was saved with a UTF-8 byte order mark (Windows PowerShell 5.1 Set-Content -Encoding utf8) | Re-save it as UTF-8 without a byte order mark (see Verifying registration) |
Service refuses to start after you replaced config.json: found a stored credential but no agentId | The replacement file dropped the agentId line | Restore the agentId line from the old file and Restart-Service TokenTimerAgent |
Real job is blocked by the local dry-run switch | execution.dryRun: true refuses real mutating jobs | First validate a signed mode: "dry_run" job, then set execution.dryRun: false to allow real work and Restart-Service TokenTimerAgent |
| Agent registers and heartbeats but never claims jobs | execution.enabled: false (observe-only) | Prepare policy, set execution.enabled: true with execution.dryRun: true, restart, and test a signed mode: "dry_run" job |
A job stays pending and this agent never claims it | The job is pinned to a different agent (the one that discovered the certificate), or requires a target selector this agent does not declare | Check the job's assigned agent; see How a job is bound to one agent |
| Renew job creation is refused for a certificate you can see in inventory | The certificate was only observed, so no agent has key custody | Install the agent on the host that actually serves it; see which certificates are renewable |
| Agent looks healthy but no automatic renewals ever happen | The CertOps maintenance worker is not running on the control plane, so no renewal jobs are created | This is a server-side problem, not an agent one; see Worker scheduling |
Related
- Install an agent (Linux) - the systemd installer, shared concepts (bootstrap tokens, agent fleet, retirement), and Linux-specific troubleshooting.
- Worked example: Windows/IIS with CNG - a full walkthrough of CNG-native enrollment and an IIS binding on Windows.
- Reference clients: debug the protocol from Bash or PowerShell - dry-run or verify the protocol without deploying the full agent.
- DNS-01 providers - credentials and zone routing for DNS-01 challenges (
windows-iisonly supports DNS-01, not HTTP-01). - The
windows-iistarget - Certificate automation - scheduled renewals, kill switch, and bulk renew.
- Executor API - how jobs are claimed, bound to an agent, and reported.
- Renew certificates automatically - profiles and the renewal badge.
- Reconciling interrupted jobs - when an agent stops reporting midway.
- Troubleshoot local policy and verification failures - when a job is rejected or fails on the agent host itself.
- Configuration reference - control-plane variables that affect the fleet.