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.txtentry that can never renew. - Certificate — one on disk has to cover the name. A wildcard you
already hold counts, and then no
domains.txtentry 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 file —
127.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, andname:8787there does not error — it simply never matches, and the name then fails to resolve with nothing to explain why.
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.comis 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.
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.
| Challenge | Proves control by | Wildcards | Needs |
|---|---|---|---|
| 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-01 | A special TLS handshake on port 443 | No | 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.
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.comdoes not matchexample.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.comcoverswww.example.combut nota.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.
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.
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.
| Provider | What you need | Where |
|---|---|---|
| Cloudflare | API Token with three permissions:
Zone Read, DNS Read and DNS Edit |
My Profile → API Tokens → Create Token |
| DNS Made Easy | API Key + Secret Key | Console → Account Information |
| NS1 | API Key with DNS read + record write | Account 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.
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
| Proves | Issued by | |
|---|---|---|
| DV | You control the domain | Anyone, including Let’s Encrypt |
| OV | Above, plus your organization legally exists | Paid CAs only |
| EV | Above, with stricter vetting | Paid 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.
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.
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.
| Tier | Check | Catches |
|---|---|---|
| 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 |
| T1 | Each 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 |
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:
| State | Meaning |
|---|---|
| served | a bind reads the expected crt-list |
| not referenced | nothing reads it. Deployed, and will never be served |
| unknown | a 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 here | a 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.
crt directory bind is not a faultIf 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
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:
| Task | Runs | What that run does |
|---|---|---|
Cert Camel Renew | every 6 hours, from the time you chose | Renews what the CA says is due, deploys each one, verifies every node is serving it |
SSL Cert Check | daily, same time | Re-reads expiry dates. Never issues or deploys anything |
Cert Camel Monthly Report | daily 08:00 | Emails 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.
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-ScheduledTaskTriggerhas 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 -RepairTasksas 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.
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
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:
| Alert | Fires when |
|---|---|
| Certificate expiring soon | A watched host crosses a configured threshold (default 30, 14, 7 days). Once per host per threshold, not on every check. |
| Renewal scheduled | The 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 succeeded | Issuance passed, and every deployment check either passed or is waiting for a bind line. |
| Automated deployment failed | Anything 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 summary | 1st 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.
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.1only 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
Hostheader 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 tosettings.jsonand 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.
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.
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:
| Setting | Default |
|---|---|
| Keep run logs for | 90 days |
| Maximum log folder size | 200 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.
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
| Symptom | Cause |
|---|---|
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
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 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.