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.
Requirements
| Requirement | Detail |
|---|---|
| OS | Windows, for the native Windows Service installer. Verified end to end (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. |
| Runtime | Node.js 22 or later (>=22.0.0 <25.0.0) |
| 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 TokenTimer. No inbound access needed. |
| Plan | A plan that includes CertOps agents (Pro or Team; per-workspace agent count of 5 on Pro, 25 on Team; see Plan limits) |
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
From an elevated (Administrator) PowerShell session, in the directory the agent package was unpacked into:
.\scripts\install-agent.ps1 `
--api-url https://tokentimer.ch `
--workspace-id <your workspace id>
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, set the environment variable first instead:
$env:TOKENTIMER_AGENT_BOOTSTRAP_TOKEN = "ttboot_..."
.\scripts\install-agent.ps1 --api-url https://tokentimer.ch --workspace-id <your workspace id>
Run with --dry-run first to print every action without changing anything on the host.
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 has been verified end to end 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. There is no HTTP-01/webroot support on either platform yet, agent-side or control-plane-side; a windows-iis target's certificate always comes from a DNS-01 issuance.
Verifying registration
Get-Service TokenTimerAgent
You should see Running, and the Agent fleet panel on the Certificate operations page flip the agent to registered. The fleet panel shows status, last heartbeat, clock drift, and version compatibility.
Upgrading and uninstalling
Upgrading follows the same idea as Linux: copy the new package over the old one and re-run install-agent.ps1 with the same flags. The state directory, credential, and keys are preserved, so the agent keeps its identity.
To uninstall:
.\scripts\install-agent.ps1 --uninstall
This stops and deletes the TokenTimerAgent service and removes the app directory. The state directory is preserved, same as on Linux; remove it yourself once you are sure:
Remove-Item -Recurse -Force C:\ProgramData\TokenTimerAgent
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://tokentimer.ch |
| 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_ENABLED is off platform-wide, or the API URL/route path is wrong | Contact support to confirm CertOps availability; double-check the API URL passed to the installer |
| Agent blocked at registration or heartbeat | Version outside the accepted window | Upgrade the agent to the current release |
| TLS handshake failure to TokenTimer | Egress proxy performing TLS inspection with a CA the agent does not trust | Reinstall with --ca-bundle, or set caBundlePath in config.json |
Installer or agent fails with an icacls error | Missing icacls, or an access list the agent did not expect on an existing state file | Confirm icacls is on PATH (present by default on Windows Server); if upgrading from a very old agent version, remove the state directory's existing ACLs and let a fresh install rebuild them |
| Jobs plan but never change anything | Still in dry-run | Set execution.enabled: true and execution.dryRun: false, then restart the service |
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 |
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 tested 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). - Automation and executors - how jobs are planned, routed, and dispatched, including the
windows-iistarget reference. - 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.