Home / Blog

← Back to Blog Engineering

Why we built a native C# IIS background worker

Why we built a native C# IIS background worker

Every certificate automation platform eventually faces the same architectural decision: what exactly runs on the customer's server? The default answer in the ACME ecosystem is to install a full-featured ACME client on every host. We deliberately did not do that. Instead we built a small .NET worker that polls for jobs, executes them locally, and reports structured results — while all orchestration stays in the control plane.

This post explains that decision and how the agent actually works.

Native Windows and IIS certificate automation architecture

The problem with a full ACME client on every host

Running a complete ACME client on each edge server looks simple until you operate a fleet.

Configuration drift. Every host holds its own account key, its own renewal configuration, and its own scheduled task. Three years later, no two servers are configured quite the same way, and the only way to find out is to log in and look.

Privilege surface. A full client needs to negotiate with the CA, solve challenges, write to the filesystem, and modify web server configuration. That is a broad set of capabilities to grant on every machine in the estate.

No fleet visibility. When renewal fails, it fails locally. Unless something scrapes logs off every host, the first symptom is a browser warning in production.

Blast radius on change. A new CA endpoint, a changed validation requirement, or an ACME client CVE means touching every server rather than one service.

The underlying issue is that this design places decision-making at the edge. We wanted the edge to make no decisions at all.

The split: orchestration centrally, execution locally

Certinite splits the lifecycle into two clearly separated halves.

The control plane owns everything that requires judgment: ACME account and order management, choosing the validation method, tracking certificate state and expiry, deciding when a renewal is due, and handling commercial CA workflows. All of this happens in the API, once, for every tenant.

The agent owns only what genuinely requires being on the machine: writing a challenge file that IIS will serve, importing a certificate into the Windows certificate store, and attaching it to the right site binding.

The agent has no schedule of its own, no ACME implementation, no knowledge of certificate authorities, and no opinion about when anything should happen. It asks the API "is there work for me?", does exactly what it is told, and reports the outcome. Adding a CA or changing validation logic is a control-plane change; the agents keep running untouched.

Outbound-only by design

The agent opens an outbound HTTPS connection to the API roughly every ten seconds and asks for its next job. That is the entire network model.

There is no inbound listener, no open port, and no firewall exception. Nothing on the internet — or on the internal network — can initiate a connection to the agent. For servers in a DMZ or behind restrictive egress policies, this is usually the difference between an approved deployment and a rejected one, because the security review reduces to a single outbound HTTPS destination.

Authentication uses a per-agent secret sent on every request. The agent receives it once during enrollment, when an operator pastes a short activation code into the installer; the API stores only a SHA-256 hash of the secret, never the secret itself.

A deliberately small job vocabulary

The agent understands a handful of job types, each of which maps to a concrete local operation:

Job What the agent does
HTTP01Challenge Writes the ACME token to the challenge directory, then verifies over loopback that IIS actually serves it
InstallCertificate Imports the issued certificate and binds it to the correct IIS site
GenerateCSR Creates an RSA 2048 key locally and returns only the CSR
InstallPremiumCertificate Pairs a commercial certificate with the local key and installs it

Keeping this vocabulary small is the point. Every job is a self-contained, idempotent operation with a structured success or failure result, which means the control plane always knows the real state of every host rather than inferring it from logs.

Native Windows, not shelled-out commands

On Windows the agent uses the platform's own management APIs rather than scripting around them.

Certificates are imported into LocalMachine\My — the same Local Machine personal store an administrator would use — with MachineKeySet and PersistKeySet, so the private key survives reboots and remains available to the IIS worker process.

Bindings are managed through Microsoft.Web.Administration, the supported IIS management API. The agent locates the site whose binding matches the domain, then updates the existing HTTPS binding or creates one on port 443 with SNI enabled, so multiple hostnames coexist on a single IP.

This matters because the common alternative is generating PowerShell or netsh command strings and parsing their output. That approach is fragile across Windows Server versions, hard to error-handle meaningfully, and effectively impossible to unit test. Calling ServerManager directly gives us typed objects, real exceptions, and behavior consistent with what IIS Manager itself does.

The HTTP-01 path is similarly IIS-aware. ACME tokens have no file extension, which IIS refuses to serve by default, so the agent writes a scoped web.config mapping extensionless files to text/plain and grants read access to IIS_IUSRS and IUSR. It then fetches the token over loopback with the correct Host header and compares the response body before telling the API the challenge is ready. The CA is only asked to validate once the agent has proven the file is genuinely reachable — which converts a large class of silent validation failures into an explicit, actionable error.

Private keys for commercial certificates never leave the server

For commercial certificates the agent generates the RSA key pair on the machine itself and sends back only the CSR. The private key is written locally and never transmitted. When the CA issues the certificate, the API sends the certificate PEM down, the agent pairs it with the key that never left, installs the result, and deletes the temporary key file.

The API orchestrates the entire purchase and issuance workflow without ever being in a position to see the key that secures the site.

One codebase, Windows and Linux

The same project targets both net8.0-windows and net8.0. Platform-specific code sits behind a single IServerManagerService abstraction, so the worker loop itself contains no platform branching at all.

On Windows that abstraction resolves to the IIS implementation described above. On Linux the same binary runs headless under systemd and resolves to nginx, Apache, Tomcat, or IBM HTTP Server depending on configuration. Job handling, polling, authentication, and result reporting are literally the same code on every platform — only the last mile of "write this file, install this certificate" differs.

What we would tell you to copy

If you are building something similar, three decisions have paid for themselves repeatedly:

  1. Put every decision in the control plane. An agent that decides nothing cannot drift. Upgrades, new CAs, and changed policy become one deployment instead of hundreds.
  2. Make the agent outbound-only. It removes an entire attack surface and, just as importantly, it removes the argument during security review.
  3. Use the platform's real APIs. Microsoft.Web.Administration and X509Store on Windows are more work up front than shelling out to a command, and they behave far better in the long run.

Not every deployment needs an agent at all. Hosting panels and appliances such as cPanel, Plesk, F5 BIG-IP and FortiGate are driven agentlessly straight from the API over their own management interfaces — the same control plane, without anything installed on the target.

Want to see it work? Create a free account and run a full issuance cycle on a lab host, or read more about automating Let's Encrypt on Windows Server and IIS.