Skip to main content
Version: 0.12

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.

Installing on Linux instead?

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 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

RequirementDetail
OSWindows, 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.
RuntimeNode.js 22 or later (>=22.0.0 <25.0.0)
PrivilegesAn elevated (Administrator) PowerShell session to run the installer (it creates the Windows Service, ACLs, and directories)
NetworkOutbound HTTPS from the host to your TokenTimer API URL. No inbound access needed.
Control planeCertOps enabled, plus CERTOPS_SIGNING_ENCRYPTION_KEY and CERTOPS_REGISTRATION_ENCRYPTION_KEY set (see Configuration)

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 SYSTEM account are granted access to agent-owned state. Nothing else is.
  • The built-in Administrators group 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: download it as part of the agent release tarball and verify the release's checksum before running anything from it.

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: download them as part of the release tarball and 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.example.com `
--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.example.com --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.

PowerShell 5.1 and PowerShell 7 are both supported

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.

PathPurpose
C:\ProgramData\TokenTimerAgent\appThe agent package, read-only at runtime
C:\ProgramData\TokenTimerAgent\stateConfig, credential, and generated keys, ACL-restricted per above
Certbot isn't packaged on Windows; acme.sh DNS-01 is

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

SymptomLikely causeFix
Get-Service TokenTimerAgent shows StoppedStartup failure, most often registration or configCheck the Windows Event Log (Application) for the service host's output, and config.json for a malformed value
Installer refuses the API URLURL is not https:Use HTTPS, or --allow-insecure-local-http for a loopback dev host only
Registration fails as unauthorizedBootstrap token already used, expired, or revokedTokens are single-use; create a new one in the dashboard
Registration fails and CertOps routes 404CertOps disabled on the instanceEnable it on API and worker (see Enable CertOps)
Registration fails referencing an encryption keyCERTOPS_REGISTRATION_ENCRYPTION_KEY unset or malformedSet 64 hex characters and restart the API
Agent blocked at registration or heartbeatVersion outside the accepted windowUpgrade the agent, or widen the version range on the control plane
TLS handshake failure to the control planePrivate CA not trusted by the agentReinstall with --ca-bundle, or set caBundlePath in config.json
Installer or agent fails with an icacls errorMissing icacls, or an access list the agent did not expect on an existing state fileConfirm 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 anythingStill in dry-runSet execution.enabled: true and execution.dryRun: false, then restart the service
A job stays pending and this agent never claims itThe job is pinned to a different agent (the one that discovered the certificate), or requires a target selector this agent does not declareCheck 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 inventoryThe certificate was only observed, so no agent has key custodyInstall the agent on the host that actually serves it; see which certificates are renewable
Agent looks healthy but no automatic renewals ever happenThe CertOps maintenance worker is not running on the control plane, so no renewal jobs are createdThis is a server-side problem, not an agent one; see Worker scheduling