How it works

Cert Camel

Watches when your TLS certificates expire, and renews them. Anything you track here is presumably something you intend to renew, so the two halves are one tool.

1. Opening it

Always use Open Tracker.bat. A page opened straight from disk (file://) genuinely cannot run PowerShell, call a DNS API, or write to its own folder — so buttons there would be decoration, and every view, including the certificate table itself, needs the session token the server hands it. Open ssl-tracker.html directly and you get an explanation, not a page.

Open Tracker.bat — or the Cert Camel shortcut beside it, which is the same launcher with the camel on it — starts a small server on 127.0.0.1 and opens the app against it. Close that console window and the server stops.

The shortcut exists because a .bat cannot carry its own icon: Windows takes it from the file association, which is per-extension and machine-wide, so giving this one a camel would put a camel on every batch file on the computer. Setup creates the shortcut and offers a copy on your desktop. It is deliberately not shipped in the download — a .lnk stores an absolute path, so one built elsewhere would point at somebody else’s folder. Re-run setup after moving the folder and it is rebuilt.

Nothing is exposed to your network: it binds to loopback only, and every request must carry a random token generated fresh each launch. Loopback alone is not access control — anything else running on the machine can reach it, which is why the token exists.

Serving this page over HTTPS

Set up during first-time setup, and normally already done by the time you read this. The hostname is the switch: Settings → General → Tracker address holds a name and a fixed port, and the console serves itself over TLS on that name using a certificate it issued. Clear the name and it goes back to plain HTTP on 127.0.0.1. There is no separate on/off control, because two controls for one meaning could disagree — and the way that disagreement got resolved was the server coming up on plain HTTP and saying so in a log nobody had open.

It stays on loopback. The name resolves to 127.0.0.1 through this machine’s hosts file and no public DNS record is needed — DNS-01 validation reads a TXT record and never connects to the host. The panel checks four things separately, because they fail in four different places:

  • DNS zone — a configured credential has to manage that zone. A token scoped to one zone cannot issue for another however plausible the name looks, and finding that out later means a domains.txt entry that can never renew.
  • Certificate — one on disk has to cover the name. A wildcard you already hold counts, and then no domains.txt entry is wanted: adding one would pull the name off the wildcard and onto the zone’s other certificate.
  • Port — fixed, not the random free port used by default. A name is no use on a port that moves every launch.
  • Hosts file127.0.0.1  tracker.example.com. Needs administrator, so the panel either writes it for you or gives you the line. No port on that line: the hosts file has no port field, and name:8787 there does not error — it simply never matches, and the name then fails to resolve with nothing to explain why.
The hostname becomes public

Every certificate’s SAN list goes to Certificate Transparency logs, so crt.sh lists it the moment it is issued, with or without a DNS record. That is true of every certificate from every authority — skipping the DNS record buys resolution reliability, not privacy.

Turning it on takes effect at the next start — whether to speak TLS at all is settled once, as the listener comes up, so saving cannot move a server that is already running. Restart it, then the URL becomes https://<name>:<port>. Plain HTTP on that port gets a temporary redirect to it, so an old bookmark still lands somewhere; temporary rather than permanent because a browser caches a permanent one, and turning HTTPS back off later would leave it redirecting to a scheme the server no longer speaks.

Which certificate it serves is not pinned the same way. A running server re-asks which one covers its own name — at once when the file it is serving changes, and otherwise every couple of minutes — so a renewal is picked up without a restart, and so is a certificate that appears somewhere it was not already watching. That second case is the one that used to need a restart: giving the console its own certificate puts the right file in a folder nothing was looking at.

If the certificate cannot be loaded at all the server says so and falls back to plain HTTP rather than refusing to start. An expired one is less serious: the browser still offers Advanced → Proceed, and renewal runs from the scheduled task rather than from this page, so the next scheduled run repairs it unattended. serve.ps1 -NoTls forces plain HTTP for anything neither of those covers. The page never sends Strict-Transport-Security — it would remove the click-through that is the way back in.

2. Adding domains

Press Edit domains on the Certificates page — saving runs a fresh check automatically, so the new host shows up in the same gesture. (The file behind it is domains.txt, and editing that by hand then pressing Check now still works.) One host per line; blank lines and # comments are ignored. Add :port to watch something other than HTTPS.

[Example Product]
example.com
www.example.com
mail.example.com:993

A line in [Brackets] starts a category, which affects only how the page is organised — groups are ordered by urgency rather than alphabetically, so whatever expires soonest floats to the top. Past a dozen rows a search box appears.

domains.txt is yours: gitignored, never overwritten. domains.example.txt is the shipped sample that setup copies from on a first run.

3. How certificates are grouped

You never configure this per domain. Certificates are grouped by DNS zone, and the zone list comes from your DNS provider — the tool asks which zones the account manages, then puts every tracked hostname under the longest zone that covers it.

example.com          ─┐
www.example.com      ─┼─  one certificate, three names
shop.example.com     ─┘

Add a fourth hostname to domains.txt and it joins that certificate automatically.

The zone list is read from the provider rather than guessed from the name because guessing is wrong in two common cases: example.co.uk is not a two-label domain, and a delegated sub-zone like dev.example.com sitting under example.com needs to win.

Which names end up on a certificate

Your tracked hostnames, plus any extra names found on the certificate currently being served. That matters: if production has a legacy.example.com nobody added to domains.txt, renewing without it would quietly break that host.

Two kinds of name are left off, and shown as Not included so the omission is visible:

  • Wildcards. *.example.com is excluded by default — among other things it rules out HTTP/2 on some routers. Opt in per certificate if you want it.
  • Names in zones you do not manage. The DNS challenge could not be completed for them, so asking would fail the whole order.
Domains with no DNS provider are flagged, not hidden

Anything that maps to no configured zone gets a no DNS badge and a callout, because a domain you cannot renew is usually a gap in the setup rather than a domain that does not matter.

4. Wildcards and challenge types

There is a widespread belief that wildcard certificates need an HTTP challenge. It is exactly backwards, and the difference decides whether wildcards are available to you at all.

ChallengeProves control byWildcardsNeeds
DNS-01
(what this tool uses)
Writing a TXT record at _acme-challenge.<zone> Yes — the only option An API token for your DNS
HTTP-01 Serving a file at /.well-known/acme-challenge/ No — cannot issue them A public IP and a reachable web server on port 80
TLS-ALPN-01A special TLS handshake on port 443No A public IP and control of port 443

The reason is structural. HTTP-01 proves control of one specific hostname by fetching a file from it. *.example.com stands for infinitely many hostnames, so there is no single file to place. DNS-01 proves control of the zone with one TXT record, which covers every possible subdomain at once.

This is good news for locked-down environments

HTTP-01 is the challenge that needs a public IP and an inbound web server — and it cannot issue wildcards anyway. DNS-01 needs neither: just outbound HTTPS to your DNS provider's API. A workstation with no public address and no web server can issue wildcard certificates perfectly well, which is precisely what this tool does.

Two things about wildcards themselves

  • *.example.com does not match example.com. A wildcard-only certificate leaves the bare domain uncovered, which is a common and confusing outage. A wildcard certificate normally carries the apex as a second name.
  • A wildcard matches one label only. *.example.com covers www.example.com but not a.b.example.com.

Asking for a wildcard

Put the wildcard in domains.txt as its own line:

[Example Product]
example.com          <- these three go on one certificate
www.example.com
shop.example.com

*.example.com        <- a SEPARATE wildcard certificate

That line produces a second, independent certificate for the zone containing *.example.com and example.com. It gets its own row in the Certificates table, its own Renew button, and its own .pem.

The apex goes on the wildcard, and comes off the other certificate

It has to be on the wildcard, since *.example.com does not match a bare example.com. Leaving it on both would put one name on two certificates of equal specificity, where only one can ever serve it — HAProxy matches whichever it indexed first and the loser silently never appears. In the example above the first certificate therefore covers www. and shop. only, and its row says so.

Do not delete the apex line from domains.txt to achieve this. A line there means two things at once — "watch this name" and "put it on the SAN certificate" — and removing it would stop the name being monitored. Both names stay watched either way; this only decides which certificate carries which.

A certificate issued before this rule existed still carries the apex until it is renewed, so its row and its deploy log say "still carries example.com — renew to apply" until you do.

The wildcard is never folded into the certificate covering your explicit names. That is deliberate rather than incidental: some routers — OpenShift among them — will not negotiate HTTP/2 against a wildcard certificate, so contaminating the explicit-name certificate would break exactly the hosts it exists to serve. There is no setting to merge them, because there is no safe version of that.

A wildcard line is not watched

Nothing answers at *.example.com, so there is no certificate to read and no expiry to measure. Wildcard lines are kept out of the expiry table entirely rather than sitting there permanently as an error, and the checker prints them as WILDCARD — renewal only.

Separately, if the certificate a host currently serves contains a wildcard you have not asked for, that name is reported under Not included on the certificate row — so you can see what the live certificate has that yours would not, and add the line if you want it.

5. DNS providers

Renewal proves you control a domain by writing a _acme-challenge TXT record (DNS-01), so it needs API access to your DNS. Open Settings and add a profile.

ProviderWhat you needWhere
CloudflareAPI Token with three permissions: Zone Read, DNS Read and DNS Edit My Profile → API Tokens → Create Token
DNS Made EasyAPI Key + Secret KeyConsole → Account Information
NS1API Key with DNS read + record writeAccount Settings → API Keys

Domains are many; DNS accounts are few. A handful of profiles usually covers everything you track. Adding another provider is a catalog entry in acme-lib.ps1, not new code — the underlying ACME client ships 88 DNS plugins.

Setup does the same check when it collects the credential, before anything is ordered. That is what makes it safe to issue from the production authority on a first run rather than from staging: proving the credential works is a better guard than avoiding production, and it does not leave you holding a certificate no browser trusts. A credential that fails can be retyped there and then — setup does not make you start again.

Test connection does two things: lists the zones it can see, and writes a real challenge record then deletes it. The second part matters, because listing zones only proves read access — and a read-only credential would otherwise pass the check and then fail partway through an order.

Do not use Cloudflare’s Global API Key

It can do anything to your entire account. A scoped API token can be limited to DNS edit on specific zones, which is the whole point.

6. Certificate authorities

Different certificates can come from different authorities — a common pattern is moving most of an estate to Let’s Encrypt while keeping payment-related certificates with a paid CA. Add authorities under Settings → Certificate authorities, then pick one per certificate from the Issuer dropdown on its row. Anything not pinned follows the default and shows default beneath the dropdown.

Accounts are kept separate per authority, so mixing costs nothing. The renewal log names the issuer for every order, and the confirmation prompt spells out which CA each certificate goes to and whether it is production or staging.

DV, OV and EV

ProvesIssued by
DVYou control the domainAnyone, including Let’s Encrypt
OVAbove, plus your organization legally existsPaid CAs only
EVAbove, with stricter vettingPaid CAs only

Browsers treat all three identically — same padlock, same encryption. Let’s Encrypt issues DV only. If your paid certificates are DV, keeping them with a paid CA is a policy, procurement or audit decision rather than a technical requirement. That can be a perfectly good reason; it just is not a constraint. You can tell them apart in the certificate details: OV and EV name your organization in the Subject, DV carries only the domain.

DigiCert CertCentral

Get the ACME directory URL and External Account Binding credentials (key ID + HMAC key) from Automation → ACME Directory URLs. Two notes: DigiCert retired its legacy ACME endpoint on 24 February 2026, so use the current service; and for OV/EV the organization must be prevalidated in CertCentral first, after which DigiCert validates domains out of band and those orders may not use a DNS challenge at all.

7. Staging first

New setups start against Let’s Encrypt’s staging environment. Staging certificates are not trusted by browsers, but they do not consume rate limits — and the limits are real: 50 certificates per registered domain per week, 5 identical certificates per week.

Run one renewal in staging, confirm it completes, then turn staging off for that authority in Settings and do it for real.

8. Certificates someone else renews

Some domains are worth watching precisely because something else renews them — a hosting provider, a platform, another team. You want to know when that automation quietly stops. What you do not want is this tool issuing a second certificate alongside it.

Press Managed elsewhere on a certificate to mark it. It then keeps being watched with all the same warnings, shows a badge, loses its Renew button, is excluded from “Renew all expiring”, and is refused by the server even if something asks for it directly. Press Renew here to undo.

9. What you get

A renewal writes into certs\<zone>\:

<zone>-full.pem    certificate + chain + private key   <- the .pem button
fullchain.cer      certificate + chain
cert.cer           certificate only
chain.cer          intermediates only
cert.key           private key (PEM, unencrypted)
cert.pfx           certificate + key (PKCS#12)

The combined -full.pem is what most appliances and load balancers want pasted in. The rest are there when something wants them separately.

Previous versions are kept

The current certificate always stays at that stable path, so download links and anything you point at the folder keep working. Each renewal copies the outgoing certificate into history\ first, stamped with the date it was written:

certs\example.com\
  example.com-full.pem          <- current, always here
  cert.cer  cert.key  chain.cer  fullchain.cer  cert.pfx  fullchain.pfx
  history\
    2026-07-29_210335\          <- the certificate that was live from that date
      about.json                   names, issuer and validity, so you can read
      ...the full file set         the folder without opening a certificate
    2026-10-20_084512\

Copied rather than moved, so a failed write leaves a working certificate at the stable path instead of nothing. Re-running against an order the CA considers current does not archive a duplicate.

History is capped, and that is deliberate

Five versions by default. Every archived version contains a usable private key, and one that stays usable until that certificate expires — so an unlimited archive quietly becomes a pile of live credentials. Set keepHistory in settings.json to change it; 0 keeps none.

Deploying the certificate is still yours to do. This tool issues and hands over; it never installs. That means the expiry date in the table will not change after you renew — what the table shows is whatever the live host is currently serving.

Renewing several at once

Renew all expiring orders every certificate inside the 30-day window, one after another. Sequential on purpose: concurrent orders against one DNS account collide on the _acme-challenge record, and serialising keeps you away from the rate limits. One failure does not abandon the rest. Expect a few minutes per certificate, mostly waiting for DNS.

10. Deploying to HAProxy

Issuing a certificate does not fix anything until it reaches the load balancers. Cert Camel pushes it over the HAProxy Data Plane API — no reload — and then checks that every node is genuinely serving it.

Why not the Runtime API directly?

The Runtime API updates a certificate live, but the change is memory only. The next reload for any unrelated reason silently reverts to whatever is on disk, and you would not find out until it expired. The Data Plane API storage endpoint writes to disk and pushes to the runtime socket, falling back to a reload only if that push fails. Durable and hitless. Needs HAProxy 2.2 or newer.

The one-time HAProxy change

HAProxy identifies a certificate by its file path. A path with a date in it cannot be updated in place — next year it is a different path, which means a config edit, which means a reload.

Before                                     After
/certs/2026.example.com/fullchain.pem  ->  /certs/example.com.pem
/certs/2027.example.com/fullchain.pem      (path never changes; contents replaced)

No history is lost — previous versions are archived under certs\<id>\history\ with an about.json, finer-grained than a folder per year. The certificates also have to sit inside the directory the Data Plane API manages (resources.ssl_certs_dir); converging them there is usually more work than the config lines. One reload to migrate, zero reloads thereafter.

Using crt-list rather than crt additionally makes adding a new domain hitless, not just renewing an existing one. Both are supported — and with a crt-list it is automatic: choose a crt-list structure on the deployment group — one list every frontend reads, or one per parent domain — and any pushed certificate the list does not reference yet is appended and hot-loaded. The directory is not a choice: a crt-list is created by uploading a filename, so the Data Plane API decides where it lands, and pressing Discover fills it in from the node. A wildcard always gets a list of its own under either structure. The deploy log says which happened per node (appended… and the running process loaded it versus already referenced), and an append the running process never picks up fails the node loudly rather than surfacing later as an unexplained verification failure.

Rather than typing that path, press Discover on the group: it asks each node which frontends terminate TLS and offers the ones every node agrees on, filling in the crt-list and port from what they report. A frontend present on only some nodes shows as partial rather than being offered — a pair configured differently is worth fixing before deploying to it. Discovery is strictly read-only.

One group, several frontends

A group answers which nodes, and what credentials. A crt-list answers where is this certificate referenced. Those are different questions, so an assignment can override the group's placement settings for a single certificate — both certificates reaching the same pair with the same credentials, but landing in different frontends. Set it from the Deployed cell: tick a group, open Overrides for this certificate, and leave anything blank to inherit the group's value.

Overrides cover placement (crtList, verifyPort, remoteName) and never credentials — those belong to the group, and the API refuses an override naming one.

Never configured the Data Plane API before? There is a step-by-step guide with the security details in Setting up the HAProxy Data Plane API.

Setting it up

Settings → Load balancers → add a group. One entry per set of nodes sharing credentials; each node is still pushed to and verified individually. Nodes go one per line:

lb1 https://10.0.0.11:5555
lb2 https://10.0.0.12:5555
dr1 https://10.9.0.11:5555  10.9.0.11

Name, then the Data Plane API URL. The optional third value is the address to verify against, for when the site is not served from the same host as the API.

Press Test on the group before relying on it. It saves first — the password lives in the encrypted store, so an unsaved card has no credential to test with — then reports per node whether it answered, which API version it speaks and how many certificates it can see.

Assigning a certificate

Click the Deployed cell on a certificate's row. That assignment is what unattended renewal uses, because at 3am there is nobody to ask.

Renew and Deploy open the same picker with that assignment pre-ticked, changeable for the run in hand:

  • Untick everything on Renew to renew only and push nothing.
  • Deploy needs at least one — deploying nowhere is not a thing.
  • Ticking a group the certificate is not assigned to deploys there anyway, which is how you reach a load balancer you have only just added.

11. How deployment is verified

“The API returned 200” is not evidence that anything is being served.

TierCheckCatches
T0 Before any push: the bundle parses, the private key matches the certificate, the chain is present, it is not expired, and it covers the expected names Pushing a broken certificate to every node at once
T1Each node's API accepted the upload Auth, network, config-version conflicts
T2 Implicit — the Data Plane API only falls back to a reload if the runtime push failed, so a node passing T3 has loaded it Written to disk but never loaded
T3 Connect to each node directly and compare the serial of what it serves The real proof: wrong crt-list entry, wrong SNI mapping, one node missed
Serial, not expiry date — and each node, never the VIP

A serial is unique per issuance, so it is the only value identifying a specific certificate. Two certificates issued the same day have indistinguishable expiry dates, which makes “days remaining went up” reassurance rather than evidence. Days remaining is still reported, because it is the number you actually want to see.

And with a floating VIP, testing the VIP only ever exercises whichever node currently holds it. A node that missed the push stays invisible until failover — precisely when you cannot afford it. That is why the Deployed column shows one pip per node.

A deployment is green only when every node passes, with one exception: a node whose crt-list no bind line reads yet is reported as waiting for a bind line, not as a failure. That is the normal way to stand up a new frontend — HAProxy will not reload against a bind naming a certificate that does not exist, so the certificate has to go first. The run exits zero and sends no failure alert, and the log hands over the bind line to add. Any other node failing does not stop the others, and the exit code is non-zero.

The Load balancers page

Verification proves a certificate is deployed. The Load balancers page answers a different question that nothing else can: is any frontend actually reading the crt-list it was deployed to?

Cert Camel writes certificate storage and crt-list entries and never a bind line. So a wrong crt-list path gives a green deployment, a certificate sitting on disk, and nothing served. Neither tier above catches it — T3 needs an address that reaches one specific node, and the API check proves a certificate is loaded rather than referenced.

Certificates are grouped by what serves them, worst first:

StateMeaning
serveda bind reads the expected crt-list
not referencednothing reads it. Deployed, and will never be served
unknowna relative path — HAProxy resolves those against its own working directory, which cannot be checked from here, so it says so rather than guessing
not managed herea TLS frontend reading a crt-list Cert Camel does not write. Not a fault — it is how you see what is still outside the tool

When a path does not match, the fix is offered in both directions, because which side is wrong depends on what you intended: point HAProxy at Cert Camel’s list (a bind edit and a reload), or point Cert Camel at HAProxy’s (a settings change, no reload — usually right when adopting a load balancer that already works). The two paths sit side by side, since a typo in a long path is nearly invisible in prose. Cert Camel still never edits your configuration; it hands you the commands.

A crt directory bind is not a fault

If a frontend binds a directory rather than a crt-list, everything in that directory is served — the certificate is fine. It simply is not hot-loaded until the next reload, and the row says so rather than reporting it as broken.

12. Unattended renewal

renew-due.ps1 renews only what the certificate authority says is due, then deploys and verifies. First Time Setup.bat can register it as a daily task.

Timing comes from ACME Renewal Information (ARI), not a number we picked. The CA tells each client when to come back, which spreads load and — during a mass revocation — lets the CA pull every renewal window forward. A hard-coded “30 days before expiry” cannot hear that. A fixed threshold is only the fallback for a CA publishing no ARI.

Try it safely first — this reports what it would renew and stops:

powershell -ExecutionPolicy Bypass -File .\renew-due.ps1 -WhatIfOnly
Only on a machine that is always on

If it sleeps or gets shut down, nothing renews and the first you hear about it is an expiry warning. Credentials are DPAPI-encrypted and bound to one Windows user on one machine, so they are re-entered on that server rather than copied to it.

The three tasks, and what each is allowed to do

All three are optional and registered by First Time Setup.bat. They run as you, in a hidden window, with StartWhenAvailable so they catch up after the PC has been off. Only one of the three can change anything:

TaskRunsWhat that run does
Cert Camel Renewevery 6 hours, from the time you choseRenews what the CA says is due, deploys each one, verifies every node is serving it
SSL Cert Checkdaily, same timeRe-reads expiry dates. Never issues or deploys anything
Cert Camel Monthly Reportdaily 08:00Emails the summary on the 1st; does nothing on the other days

Setup suggests a few minutes past the hour rather than the hour itself, because ACME rate limits are per-CA and shared by everyone, and the top of the hour is where every naive scheduler piles up. The six-hourly repeats inherit those twenty past.

Seeing and changing them

These are ordinary Windows scheduled tasks — nothing about them is private to Cert Camel. Press Win+R, run taskschd.msc (or search “Task Scheduler” in Start), and click Task Scheduler Library. All of them are in that root list, not in a subfolder. Right-click any one for Run, End, Disable and Properties, where the Triggers tab holds the time.

The times are yours to change. Setup only sets where they start. Nothing in Cert Camel depends on those exact times, and the Home page reads the schedule back out of Windows rather than assuming the defaults — so a time you change here shows up there, which is the quickest way to confirm the edit took.

Renewal repeats every six hours from that start, so it also runs at 09:20, 15:20 and 21:20. That is not about renewing sooner — a renewal is refused until the authority's window opens — it is so a run that fails has another chance the same day, instead of waiting twenty-four hours. It costs nothing at the authority: the check is an unauthenticated ARI request, which is not rate limited, and no order is placed until a certificate is genuinely due.

An existing install keeps the schedule it was registered with. Updating Cert Camel does not touch a task that already exists, and setup.ps1 -RepairTasks deliberately preserves whatever schedule is there. To move an older install onto the six-hourly renewal, re-run First Time Setup.bat as administrator and answer yes when it offers to register unattended renewal.

Turn on task history while you are in there

Enable All Tasks History in the right-hand Actions pane. It is a global Windows setting and it is off by default, so the History tab on every task is empty until you switch it on — which is exactly the wrong thing to discover halfway through working out why something did not run.

Three things to know before editing:

  • Changing the monthly report’s day does nothing. It is a daily task that checks the date and exits on any day but the 1st, because New-ScheduledTaskTrigger has no clean monthly trigger to reach for. Moving it to the 5th just means it runs on the 5th and does nothing. Change its time freely; the day is decided in the script.
  • Watch the password checkbox. The three unattended tasks are registered S4U — Run whether user is logged on or not with Do not store password ticked. If that box gets cleared while you are in Properties, Windows stores a password instead and the task breaks the next time that password changes. First Time Setup.bat -RepairTasks as administrator puts the principal back and keeps whatever schedule you set, because it re-registers each existing task with its own trigger and settings intact.
  • The task stores an absolute path. Move or rename this folder and the tasks still point at the old one: renewal quietly stops and the first symptom is an expiry warning weeks later. The Home page flags this when it sees it; re-running setup after a move is the fix.

The Automation panel on Home

None of the above has to be taken on trust. The Home page reads what the Windows scheduler actually has registered. The Automation box lists each service and when it runs — a list rather than a sentence, because the three run at three different times and any one-line summary of them is wrong:

AUTOMATION                        On
Renew and deploy   every 6 hours, from 12:20 AM
Expiry check                   daily 12:20 AM
Monthly summary email     not set up

On refers to unattended renewal specifically — the only one of the three that can change a certificate. It reads Off when the task is registered but disabled, Not set up when it was never registered, and Unknown when the scheduler could not be read, which is deliberately not the same as Off.

Automated renewals scheduled gives the date and time each certificate is next due, in this PC’s local time, and what happens afterwards:

*.camelnuggets.com
  Sat, Oct 3, 2026, 1:13 PM EDT
  deploys to Haproxy-Home-Lab

camelnuggets.com
  Sun, Oct 4, 2026, 8:48 AM EDT
  no load balancer assigned, so it will not deploy

Those dates are the CA’s own ARI window, and they move — they are rechecked nightly. If the forecast is missing or has gone over 36 hours stale, a Work it out now button appears, running the -WhatIfOnly dry run above; it issues nothing and touches no load balancer. When the forecast is current there is no button at all. The page is read-only otherwise: it reports on the tasks, it does not administer them.

Three things it exists to catch, each raised as an alert at the top of Home

A certificate with no load balancer assigned. It renews, and the new certificate sits on disk. renew.ps1 logs it and carries on, so “automation is on” reads as more reassuring than it should for that certificate.

A task pointing at another folder. The task stores an absolute path, so copying this folder leaves the old copy scheduled. Renewal silently stops and the first symptom is an expiry warning weeks later.

The scheduler being unreadable, reported as exactly that rather than as “off” — the second would be a lie in the direction that gets certificates expired.

13. Email alerts

Or decide not to have any

Disable alerts, beside Send to, records that as a decision. Each alert below it already has its own switch, so turning them all off is a complete off-switch already — what it is not is distinguishable from never having configured email, which is what a fresh install looks like. Ticking it says which, and stops the setup checklist on the Home page asking.

It suppresses nothing on its own: saving with it ticked simply turns the switches off, so what is stored matches what the box claims. Worth being deliberate about — with no email, a renewal that starts failing is only visible to somebody who opens this page.

Under Settings > Alerts, five independently switchable alerts:

AlertFires when
Certificate expiring soonA watched host crosses a configured threshold (default 30, 14, 7 days). Once per host per threshold, not on every check.
Renewal scheduledThe run before a renewal happens: which certificate, which names, when, and which load balancers it will be pushed to. Sent once per renewal cycle, not once per run.
Renewal succeededIssuance passed, and every deployment check either passed or is waiting for a bind line.
Automated deployment failedAnything on the unattended path did not fully succeed — the one that matters most, since it is the only signal an unattended renewal has stopped working.
Monthly summary1st of the month: everything due within 31 days, and anything currently failing.

The mail server needs a host, port, and at least one recipient. A username and password are optional — leave “requires a username and password” unticked for an internal relay that accepts mail from this machine with no login, common on a corporate network. A password you do supply is stored DPAPI-encrypted like every other credential here, never sent back to the browser, and a blank password field on a later save means keep the one already stored, not clear it.

Only two encryption modes are offered

STARTTLS (port 587, typical) and none. Implicit TLS — a server that expects encryption from the first byte, historically port 465 — is not offered: PowerShell’s mail client does not support it reliably, and a security control that sometimes silently fails is worse than one that plainly is not there. If your provider offers both, use STARTTLS.

A Send test email button saves first, then sends — the same pattern the DNS and deployment tests use. Alerting can never fail a renewal: every send is wrapped so a bad mail server gets logged and nothing else changes.

The monthly summary is a daily scheduled task that checks the date and does nothing on every day but the 1st — the ScheduledTasks module has no clean monthly trigger to reach for instead. First Time Setup.bat can register it.

14. Security

The short version. Security is the long one — what the loopback binding and the token actually protect against, how credentials are stored and what breaks when the account running this changes, what reaches the logs, and what needs administrator. It lives beside this page rather than in it because it is the part people need to read before deciding, not while looking something up.

  • The server listens on 127.0.0.1 only and requires a per-launch random token. That is still true with HTTPS turned on — the hostname resolves to loopback through the hosts file and nothing binds to a network address.
  • The token is handed to the page on the address line and taken straight back out, so no live credential sits in history or in a screenshot. Requests also have to carry a Host header this server recognises, which is what a page rebinding its own name to loopback cannot forge.
  • DNS credentials are encrypted with Windows DPAPI in secrets.xml, scoped to your Windows account on this machine. They are never written to settings.json and never sent back to the browser — the page is told only whether a secret exists. A blank credential field means “keep what is stored”, so an unchanged form cannot wipe a credential nobody retyped.
  • Nothing here — domains, credentials, certificates — is committed to git, so the folder can be handed to someone else clean.
Private keys are the real exposure, not the API keys

certs\ and acme-state\ hold unencrypted private keys — that is how PEM works, and every ACME client is the same. They are gitignored, but gitignore does not stop file sync. If this folder is inside OneDrive, Dropbox or similar, exclude those two folders from syncing. Download also puts a private key in your Downloads folder in the clear; delete it once deployed.

That download is the only thing the API hands back that is secret — every stored credential returns as a yes/no that one exists, never as itself. So it is fetched by the page, with the token in a header, rather than by following a link: there is no address anywhere that produces a private key.

15. Logs and the audit trail

The Logs page is read-only, and deliberately so — a page that could edit the record would not be worth much as a record. It shows two things that are easy to confuse.

Run logs

The narrative of one run: every ACME step, every push, every verification tier. This is what you read when something went wrong and you want to know where. They live in jobs\ and are named for what they are and when they ran — 2026-08-05T032000Z-renew-due.log.

Scheduled runs write these too. They did not before: all three tasks launched hidden with no redirection, so the unattended renewal produced no narrative at all. The runs nobody watches were the ones with no record, which is backwards.

The audit trail

One line per state change, append-only, in audit.log:

when                  who    source  event     object            outcome  detail
2026-08-05T09:47:25Z  ULTRA  task    renew     camelnuggets.com  ok       issued serial 05B3DC…, expires 2026-11-03
2026-08-05T09:47:27Z  ULTRA  task    deploy    camelnuggets.com  ok       lb1 serving, lb2 serving
2026-08-05T10:02:11Z  ULTRA  ui      settings  general, logs     ok       2 section(s) updated

who is the Windows user, because in an assessment that is the answer to the question. source separates ui from task — whether a person or the scheduler made the change, which is usually what gets asked next about any given line.

A scheduled sweep records itself even when nothing was due, so a quiet stretch means “nothing needed doing” rather than “the scheduler stopped firing”. Telling those two apart is most of the point.

What never reaches either log

No credential value, no private key, no certificate body. Credential changes are recorded by key name only — fake-pair:password removed.

Every stored credential is masked as [redacted], so a debug line added later cannot leak one by accident. Where that happens depends on who is writing: the audit trail and a scheduled run are masked as the line is written, while a run started from this page is the child process’s own output, captured whole — so it is masked when the log is read, and the file itself is swept once the run finishes.

Retention

Two settings under Settings → General, applied to run logs:

SettingDefault
Keep run logs for90 days
Maximum log folder size200 MB

Whichever is reached first trims oldest-first. Per-certificate state files (deploy-<id>.json) are current state rather than history, so they are never trimmed. Every trim writes its own audit line, which lets the log account for its own gaps instead of just having them.

The audit trail is exempt from both limits

It rotates to audit-<stamp>.log when large, and the archives are kept. Deleting audit records to reclaim disk is precisely what an assessor would object to, and a silent exception would be worse than a stated one — so it is stated, on the Logs page as well as here. If a retention policy genuinely requires audit expiry, that wants to be its own explicit setting rather than a side effect of a disk cap.

16. When it breaks

SymptomCause
Renewal fails with 403 Forbidden right after “Adding _acme-challenge…” The DNS credential cannot write the record. On Cloudflare the usual cause is a missing DNS Read — adding a record looks for an existing one first, so a token with only DNS Edit fails on that lookup and never reaches the write. Three permissions are needed: Zone Read, DNS Read, DNS Edit. Setup and Test connection both catch this before a renewal does.
DNS Made Easy returns a bare 403 Usually clock drift — it rejects any request whose timestamp is more than 30 seconds off from theirs. The tool detects this case and says so explicitly.
“Already current — nothing was re-issued” Not an error. The CA still considers the existing certificate fresh, so it declined to issue a duplicate. Wait for the renewal window, or force a re-issue.
A domain shows no DNS No configured provider manages its zone. Either add the provider, or mark the certificate as managed elsewhere if something else renews it.
Page shows data but no buttons It was opened from disk rather than through Open Tracker.bat.
Every check reports Error Nothing is listening on those hosts. Renewal still works — a DNS-01 challenge does not need a running website.

Renewal logs live in jobs\ and include the script line number on failure, which is usually most of the diagnosis.

17. The 47-day future

Under CA/Browser Forum ballot SC-081v3, the maximum certificate lifetime dropped to 200 days on 15 March 2026, falls to 100 days in March 2027, and to 47 days in 2029. This applies to every public CA, paid ones included, and domain validation reuse periods shrink alongside it.

Whatever you renew by hand today needs doing roughly twice as often next year, and about eight times as often by 2029. That is the actual argument for automating this.

18. Files

The folder is split in two, and the split is the answer to “what am I supposed to touch?” Everything at the top level is either something you run, something you read, or something that is yours. The program itself lives in resources\, and you can go your whole life without opening it.

Open Tracker.bat      start the server and open the page  <- use this
First Time Setup.bat  one-time setup
Check Now.bat         refresh the data
sos-plain-http.ps1    turn HTTPS back off when the console will not load

readme.html           this page
haproxy-setup.html    step-by-step HAProxy Data Plane API guide
windows-server-setup.html  installing where nobody is signed in
security.html         what is protected, what is not, and what is on your disk
console-certificate.html   the certificate this page is served with, and the ways back in
VERSION               what this copy calls itself
LICENSE               MIT

domains.txt           the list you edit                   (yours, gitignored)
domains.example.txt   the shipped sample

uninstall.ps1         undoes what setup did OUTSIDE this folder - run before deleting it
sos-plain-http.ps1    the way back in when HTTPS itself is the problem

resources\            everything the program needs to run - not yours to edit
  serve.ps1           the local server
  check-ssl.ps1       the checker (parallel, records SANs and serials)
  renew.ps1           performs one renewal, then deploys it
  deploy.ps1          pushes to load balancers and verifies every node
  renew-due.ps1       renews whatever the CA says is due, sends expiry alerts (scheduled task)
  monthly-report.ps1  sends the monthly summary email, if turned on (scheduled task)
  check-lb.ps1        reads what each load balancer node is actually serving
  acme-lib.ps1        shared settings, secrets, grouping and alert-sending logic
  setup.ps1           what setup runs
  issue-tracker-cert.ps1   issues the console's certificate with a hand-made DNS record
  import-console-cert.ps1  puts a certificate you already have where the console looks
  new-lb-api-cert.ps1      issues a certificate for a load balancer's Data Plane API
  ssl-tracker.html    the app shell (sidebar + view containers; needs the server)
  assets\app.css      all styling
  assets\app.js       router, API client, shared state, the job runner
  assets\views\       one file per sidebar page (home, certificates, settings, docs)
  lib\                Posh-ACME, pinned to one version and hash-checked on download
  tests\              the jsdom and PowerShell suites

generated, none committed - all of it stays at the top level, where you can see it:
  ssl-data.js         checker output
  settings.json       your configuration
  secrets.xml         DNS/SMTP credentials, DPAPI-encrypted
  zones.json          cached zone list from your DNS provider
  alert-state.json    which expiry thresholds have already been emailed, per host
  audit.log           one line per state change, append-only
  server.log          the headless server's own diagnostics
  certs\              issued certificates and private keys
  acme-state\         ACME account and order state
  jobs\               renewal logs
Moving or updating this folder? Re-run setup

A scheduled task stores the absolute path of the script it runs, so copying this folder elsewhere — or taking an update that moves a script, as the move into resources\ did — leaves every task pointing at where the script used to be. The task still reports healthy and renewal silently stops.

Home flags this on sight, and the repair is one command from an elevated PowerShell: resources\setup.ps1 -RepairTasks. It rewrites each registered task to the current location and changes nothing else about its schedule.

Requires Windows PowerShell 5.1, which ships with Windows. Nothing is installed system-wide: Posh-ACME is downloaded into resources\lib\ inside this folder, pinned to one version and checked against a recorded hash before it is used. Setup itself needs administrator — for the scheduled tasks, the hosts file entry and the permissions it puts on this folder — but the tasks it registers run unelevated.

19. Updating

This copy names itself in VERSION at the root of the folder, and Settings → General → Update shows it back to you next to whatever else it could work out.

If you cloned the repository

Check for updates fetches your remote, compares, and offers Update now when there is something to take. That only ever fast-forwards — never a merge — so on a machine running unattended the worst outcome is a refusal rather than a conflicted working tree nobody is sitting in front of. It refuses, on purpose, when a tracked file has local edits, or when this copy has commits the remote does not.

Nothing of yours is in the repository — settings, secrets, domains, certificates and logs are all gitignored — so an update replaces code and leaves your data alone.

The running page is the old code until you restart it

The panel says so after an update, and it means it: the server read those files into memory when it started.

If you downloaded a ZIP

There is no clone to pull from, so the panel asks GitHub for the newest published release and shows you both version numbers. It compares them for difference, not order — the version scheme here is not semver, and guessing an order would eventually tell you that you were current when you were a release behind. If they differ, download the new one and read the two numbers yourself.

That release lookup is the only request Cert Camel makes to a host you did not configure, and it only happens when you press the button — never on a schedule, and never from a clone, which has a better answer already. It sends nothing about your install. Security has the detail.

A check that cannot complete says so

No network, a proxy in the way, DNS down, a credential problem, GitHub having a bad morning — the row goes grey and tells you which. It will not show a tick over a failure, because “the check failed” and “you are up to date” are not the same statement, and a tool you only look at twice a year has to be trusted the twice you do.

20. Removing it

uninstall.ps1 undoes what setup did outside this folder, and needs administrator. Run it before deleting the folder, not after — everything it needs to know is read from here, and once the folder is gone what it would have cleaned up stays behind:

powershell -ExecutionPolicy Bypass -File .\uninstall.ps1 -DryRun   # shows what it would remove
powershell -ExecutionPolicy Bypass -File .\uninstall.ps1           # does it

-DryRun needs no elevation, because looking changes nothing.

Only things Cert Camel created are touched, and each is identified rather than guessed: a scheduled task must run a script inside this folder, a hosts line must carry the comment Cert Camel wrote above it, and a certificate must be the private CA this tool generates or something it signed. Anything else sharing a name is left alone — including hosts entries you added by hand, which it lists rather than removes.

It does not delete the folder

That is where the private keys are, and a script that erases key material while tidying up is one that will eventually erase the wrong folder. Delete it yourself once the run finishes.

Two things it cannot reach. Certificates already issued stay valid and stay known to the authority — nothing here revokes them, and anything already deployed to a load balancer keeps serving until it expires. And if HSTS was ever enabled, your browser still refuses plain HTTP for that name until the policy expires; the run prints how to clear it.