Six stages, six files
Nothing is passed in memory. Each stage reads a file the previous one wrote and writes a file the next one reads. Every stage can also be run on its own, which is why the handoffs are files rather than function calls.
renew-due re-runs the checker whenever domains.txt is newer than
ssl-data.js, rather than working from a host list that has since changed.| Stage | Reads | Writes | What it decides |
|---|---|---|---|
check-ssl | domains.txt | ssl-data.js |
What each host is serving today, and when it expires |
renew-due | ssl-data.js | sweep verdict | Which certificates exist, and which are due |
renew | sweep verdict | certs\ |
Obtains the certificate from the authority |
deploy | certs\ | deploy record | Puts it on each node and proves it is serving |
check-ssl connects to each host on the port domains.txt names —
host:port, defaulting to 443 — and records what is being served now. That is how a
certificate obtained somewhere else is still watched, and how expiry is known for something Cert
Camel has never issued.
Hosts become certificates
You list hostnames. A certificate authority issues certificates, each covering one or more names. Turning the first into the second is the decision everything downstream inherits.
Hosts are grouped by DNS zone, because a zone is what one DNS credential can prove control of.
Each zone produces one to three certificates, and each gets an identifier — its
certId — which becomes its folder name, its file name, and the name of the
crt-list it goes into.
*.example.com does not match example.com, so a wildcard certificate must
carry the bare name too. If the SAN certificate carried it as well, both would answer to it with
equal claim, HAProxy would pick one, and the other would silently never be used.Why the console is separate
Cert Camel serves its own page over HTTPS with a certificate it obtains like any other. Keeping it on its own means adding or removing a production hostname never re-issues it, and it is never installed on a load balancer, so its name does not travel anywhere nobody asked it to go. See The console certificate.
Certificates somebody else renews
A certificate can be marked managed elsewhere. It is still watched — so you find out when whatever renews it stops — but Cert Camel never orders it and never counts it as a gap in its own coverage. Issuing a second certificate for a name another system is already renewing is a good way to have two systems fighting over one name.
When it renews
A sweep runs on a schedule and decides, per certificate, whether to renew now. It asks the certificate authority rather than counting days: authorities publish a renewal window per certificate (ARI), which spreads renewals out instead of stacking everyone on the same threshold.
Let's Encrypt allows 50 certificates per registered domain per week, and only 5 identical certificates per week. A loop that reissues on every run burns the duplicate limit in an afternoon and then cannot renew anything for seven days. Asking the authority when it wants the next renewal avoids the question entirely.
Where files land on a node
Certificates reach HAProxy through its Data Plane API, a REST interface that writes files and reloads configuration on the operator's behalf. Two of its behaviours shape everything here.
The API chooses the directory. A file is uploaded by name, not by path, so a path pointing anywhere else describes a file that will never exist. The directory is discovered from the node rather than typed — which is why the crt-list setting offers a structure and fills the directory in for you.
The API rewrites names. Dots in a filename become underscores, so
example.com.pem is stored as example_com.pem. Anything referring to that
file afterwards — a crt-list entry, a bind line, a later lookup — must use the stored
name, not the requested one.
Why a wildcard is always kept apart
Anything reading one list can serve everything in it. Putting a wildcard beside the certificates it overlaps means a frontend intended for one can serve the other, and the first entry in a list is what an unrecognised request falls back to. Separate files keep that choice explicit.
| Structure | SAN certificate | Wildcard certificate |
|---|---|---|
crt-list.txt | crt-list.txt | wildcard-crt-list.txt |
{certId}-crt-list.txt | example.com-crt-list.txt | wildcard.example.com-crt-list.txt |
A deployment group holds the nodes and the credentials to reach them. A certificate is assigned to one or more groups, and the crt-list setting on the group decides the filename inside each. A single certificate can be pinned to a different file on one group than on another; an explicit per-certificate override is left exactly as written, because it is already a deliberate answer.
Proving it is really serving
An API returning success means the file was accepted, not that anything is using it. So each node is checked afterwards, in tiers.
| Tier | Question | When |
|---|---|---|
T0 | Is the bundle valid at all? | before anything is pushed |
T1 | Did the node's API accept it? | per node |
T3 | Is the node actually serving it, by serial? | per node, per name |
There is no T2. "Present in HAProxy's memory" is covered implicitly: the Data Plane API pushes to the runtime socket and only falls back to a reload if that fails, so a node passing T3 has necessarily loaded it.
How a wildcard is proved
A wildcard cannot be proved by asking for the name it covers, because several certificates may
answer to that. It is probed with a synthetic certcamel-probe.<apex> that only a
wildcard can match. The bare domain is probed too, but only for coverage — it says who is
serving a name more than one certificate may legitimately hold.
Three outcomes
| Outcome | Means | Do |
|---|---|---|
| serving | The node is answering with this exact certificate | Nothing |
| awaiting bind | Installed, but no configuration points at it yet | Add the bind line, reload HAProxy |
| failed | Rejected, or serving something else | Read the log — the node said why |
HAProxy refuses to load a configuration naming a certificate file that does not exist. A brand-new frontend therefore has to be built in two steps: put the certificate on the node first, then add the configuration pointing at it. A deployment that ends here has done everything asked of it, so it reports success, sends no failure alert, and hands over the line to add. A node already proved to be serving is never reported this way, even if some other list is unread — the state means nothing could be verified, not that a file is unreferenced.
Where two parts must agree
Four values are produced in one place and consumed in another. Each is a plain string, so a mismatch is silent: nothing fails, the two sides simply describe different things.
| Value | Produced by | Consumed by | If they disagree |
|---|---|---|---|
| The certificate identifier | the grouping, from the watched hosts | the folder, the filename, the crt-list name, renewal | a certificate is filed under a name nothing looks for |
| The crt-list path | deployment, from structure + identifier | the load balancer page's reporting | a working deployment is reported as never going to be served |
| The stored filename | the node's API, after rewriting dots | crt-list entries, bind lines, later lookups | a bind line names a file that does not exist |
| The deployment outcome | one deployment run | a log, an email, an audit entry, two pages | the same event is green in one place and red in another |
A certificate's identifier is computed from the hosts being watched, so it can change when the watched set changes — giving the console its own address, for instance, moves its name onto a certificate of its own with a new identifier. Anything holding an identifier from before must re-derive it rather than remember it.