Manual · Operator Reference

cert-helper

Full TLS certificate lifecycle for CommuniGate Pro: discover every domain on the server, filter by eligibility, order certs from Let’s Encrypt via Cloudflare DNS-01, install them back into CGP with a read-back fingerprint check, renew automatically before expiry, notify the postmaster on failure. Ships as a single hardened binary (Windows / Linux) with an embedded operator dashboard.

cert-helper · CommuniGate Pro 6.4+ · ACME (Let’s Encrypt) · Cloudflare DNS-01 · September 2026

Contents

  1. What cert-helper is
  2. The renewal pipeline — end to end
  3. Requirements before install
  4. Install — Windows & Linux
  5. Configuration reference (TOML)
  6. First run & smoke test
  7. Web dashboard tour
  8. Eligibility filter
  9. Auto-renewal scheduler
  10. Postmaster notifications
  11. Cloudflare DNS-01 setup
  12. HTTP-01 alternative
  13. Staging vs production
  14. Troubleshooting
  15. Uninstall
  16. FAQ & glossary
Section 1

What cert-helper is

A daemon that automates every step of getting a public TLS certificate on a CGP server, and keeps them fresh forever after.

CommuniGate Pro ships with only a manual TLS surface: an admin generates a private key in the web UI, pastes in a CSR or certificate, and files it under the domain’s SecureCertificate. No ACME. No Let’s Encrypt integration. No renewal scheduler. Every 90 days, someone remembers — or they don’t, and the browser certificate error page shows up on a Monday morning.

cert-helper replaces all of that with an unattended service. Every 24 hours (configurable) it walks the CGP CLI, finds every domain whose cert is inside the renewal window, orders a fresh one from Let’s Encrypt using DNS-01 via Cloudflare (or HTTP-01 via a static webroot), converts the key to the format CGP requires (PKCS#1 traditional RSA), pushes it back over the CGP CLI, and reads the installed cert back to confirm the fingerprint matches what was sent. If anything fails, an email lands in the postmaster mailbox with the domain name, timestamp, and error.

It also handles the “which domains SHOULD get a public cert” question. A CGP server usually has dozens of domains — a handful are yours, the rest are customer domains whose MX records point to your server but whose DNS you don’t control. Trying to issue certs for those would burn Let’s Encrypt rate limits and fail public CT log review. cert-helper’s eligibility filter (DNS lookup + local-IP check + allow/deny list) sorts them out, and (optionally) pushes CertificateType=Disabled to CGP for the ineligible ones — replacing any external “keep applying Disabled” script.

Section 2

The renewal pipeline — end to end

Every renewal (manual or scheduled) runs the same 12-step pipeline. Understanding it makes troubleshooting simple — every step logs to cert-helper.log, so a failed run tells you exactly where it stopped.

1
Load / create ACME account. First run creates an LE account against the configured directory URL, contact email, and stores the credentials in the state DB. Every subsequent run reuses them.
2
Refresh CGP settings for the target domain. Fresh DNS may have changed, aliases may have been added.
3
Re-evaluate eligibility. Allow/deny gate → DNS A-record lookup → local-IP intersection. A domain that was eligible last week but whose DNS moved to Cloudflare in the meantime is dropped from this run and marked ineligible.
4
Collect SAN candidates. Base domain + DomainAliases array + host part of Preferred URL for HTTP. Each is passed through the eligibility filter individually; failing aliases are dropped (partial-cert fallback), surviving ones go into the SAN list.
5
New ACME order. Multi-SAN order posted to LE.
6
DNS-01 challenge setup. Cloudflare API create-TXT-record at _acme-challenge.<domain> with the ACME key-authorization hash. Record ID stored for cleanup.
7
Propagation wait. 45s hard wait — enough for Cloudflare’s authoritative nameservers to serve the fresh TXT to Let’s Encrypt’s validators.
8
set_ready + poll. Tell LE the challenge is ready; poll the order status with backoff until it’s Ready (validated) or Invalid (failed).
9
Cleanup. Delete the TXT record via CF API. Runs even on failure so we don’t leak orphans.
10
Generate RSA-2048 CSR. Fresh keypair per cert (not reused). CGP requires traditional RSA (PKCS#1) so we generate with the rsa crate directly and hand the key to rcgen for the CSR.
11
Finalize + fetch chain. POST the CSR, poll poll_certificate, receive the PEM chain.
12
Install into CGP + read-back verify. Convert PKCS#8 → PKCS#1, strip PEM headers, base64-encode, UPDATEDOMAINSETTINGS <domain> { SecureCertificate=...; PrivateSecureKey=...; CAChain=...; CertificateType=Enabled; }. Immediately re-read via GETDOMAINEFFECTIVESETTINGS and compare SHA-256 fingerprints. Mismatch = fail loudly. Match = state DB updated with new fingerprint, not_after, installed_at.
ⓘ
Typical wall-clock for a successful run: 50–70 seconds, dominated by the 45s DNS propagation wait. The ACME round-trips and CGP install combined are ~10 seconds.
Section 3

Requirements before install

Section 4

Install — Windows & Linux

Windows

# 1. Create the install directory
New-Item -ItemType Directory C:\CommuniGate\Helpers\cert-helper -Force

# 2. Drop the binary + license
Copy-Item cert-helper.exe   C:\CommuniGate\Helpers\cert-helper\
Copy-Item license.key       C:\CommuniGate\Helpers\cert-helper\

# 3. Create the config from the template
Copy-Item cert-helper.toml.example C:\CommuniGate\Helpers\cert-helper\cert-helper.toml
notepad C:\CommuniGate\Helpers\cert-helper\cert-helper.toml
# see § Configuration reference

# 4. Register as a Windows Service (auto-start, restart-on-crash)
cd C:\CommuniGate\Helpers\cert-helper
.\cert-helper.exe --install-service
sc.exe start cert-helper
sc.exe query cert-helper

The service runs as LocalSystem. If you prefer a dedicated account, edit the service after install with sc.exe config cert-helper obj= "NT AUTHORITY\NetworkService".

Linux (Fedora / RHEL / Debian / Ubuntu)

The deploy tarball at /downloads/cert-helper/ contains a self-contained install-linux.sh that does everything below in one step. Or by hand:

sudo mkdir -p /opt/cert-helper /var/lib/cert-helper /var/log/cert-helper
sudo install -m 0755 cert-helper       /opt/cert-helper/cert-helper
sudo install -m 0600 license.key       /opt/cert-helper/license.key
sudo install -m 0600 cert-helper.toml  /opt/cert-helper/cert-helper.toml
sudo vim /opt/cert-helper/cert-helper.toml  # see § Configuration reference

sudo tee /etc/systemd/system/cert-helper.service <<'EOF'
[Unit]
Description=CGP Certificate Helper
After=network-online.target CGPro.service
Wants=network-online.target

[Service]
Type=simple
ExecStart=/opt/cert-helper/cert-helper --config /opt/cert-helper/cert-helper.toml
WorkingDirectory=/opt/cert-helper
Restart=always
RestartSec=30
User=root

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now cert-helper
sudo systemctl status cert-helper
Section 5

Configuration reference (TOML)

Every section is optional at load time (defaults fill in what you omit) but you almost always want to review at least [cgp], [policy], [acme], and [cloudflare]. All master switches (SMTP send, auto-disable, ACME production URL) default to their safe value — nothing reaches out or mutates CGP until you opt in.

[cgp] — CommuniGate CLI connection

KeyDefaultNotes
hostlocalhostUsually the daemon runs on the CGP host itself.
port106Standard CGP CLI.
admin_user—Required. Usually postmaster@<main-domain>.
admin_pass—Required. Plaintext in TOML; file should be mode 0600, root-owned.

[db] — SQLite state store

KeyDefaultNotes
pathstate.sqliteAbsolute path recommended when running as service.

[log] — File-based tracing

KeyDefaultNotes
dir.Absolute path recommended.
filecert-helper.logFilename inside dir.
levelinfotracing-subscriber env-filter, e.g. info,cert_helper=debug.
⚠
Stdin/stdout are reserved for the CGP helper-protocol channel — never log there or CGP kills the process. All output goes to the log file. This is why there’s no tail -f on stdout by design.

[policy] — Eligibility filter

KeyDefaultNotes
modeallow_all_exceptallow_all_except = default-eligible minus excludes. deny_all_except = default-ineligible plus includes.
list[]Domain names for the exclude/include set.
local_ipsautoIPv4/IPv6 addresses of the mailserver. A domain is eligible only if at least one of its resolved A/AAAA records is in this set. Auto-discovered from AdminDomainName when omitted.
auto_disablefalseWhen true, push CertificateType=Disabled to CGP for domains just flipped to Ineligible. Replaces the customer’s external keep-applying-Disabled script.

[acme] — Let’s Encrypt account

KeyDefaultNotes
directory_urlLE stagingAlways staging by default. Switch to production only after end-to-end staging success (see § Staging vs production).
contact_email—Must contain a dot. LE rejects contactless-form addresses.
http01_dir./acme-challengesUsed only by HTTP-01. Ignored when [cloudflare] is present.

[renewal] — Background scheduler

KeyDefaultNotes
enabledtrueMaster switch for the daily task.
interval_seconds86400Once every 24 hours is what you want in production. Testing? Set to 60 briefly to see it fire.
threshold_days30Renew when not_after < now() + this. LE recommends 30.

Retry backoff on per-domain failure: 1h → 6h → 24h → 24h → fail. After the 5th consecutive failure the row flips to failed; the scheduler skips it; an operator can reset by clicking Renew now on the dashboard.

[smtp] + [smtp.events] — Postmaster notifications

KeyDefaultNotes
enabledfalseOff by default. Turn on when you have a working relay.
host / portlocalhost / 25Hand-rolled SMTP client, no auth, no TLS. Assumes a localhost relay. If you need auth or STARTTLS, an lettre-backed variant is a small refactor.
helo—HELO name.
from—Envelope + header From.
recipients—Array of To addresses.
subject_prefix[cert-helper]Prepended to every subject.
events.issuance_failedtrueEnd-to-end failure to obtain a cert.
events.sans_shrunktrueNew cert covers fewer SANs than the old one.
events.no_dnstrueConfigured domain / alias doesn’t resolve.
events.not_localtrueResolves but IP is not in local_ips.

[web] — Operator dashboard

KeyDefaultNotes
enabledtrueServes the read-write dashboard.
addr127.0.0.1:8765Bind loopback. There is no built-in auth — expose externally only via an authenticated reverse proxy.

[cloudflare] — DNS-01 responder

KeyDefaultNotes
api_token—Scoped Zone.DNS:Edit token. Never a global API key.
zone_id—Single-zone scope. Multi-zone support is a small follow-up if you need it.

Omit the whole [cloudflare] section to fall back to HTTP-01. See § HTTP-01 alternative.

Section 6

First run & smoke test

✓
Successful first run on staging = the pipeline is proven end-to-end for that domain. Only then switch directory_url to production (see § Staging vs production).
Section 7

Web dashboard tour

Read-write UI at http://127.0.0.1:8765. Auto-refreshes every 5 seconds.

Top strip

License status, server time, config file path, DB path, CGP host, policy mode, local IPs (both configured and auto-discovered).

Counts row

Total domains, and per-status counts (Active, Pending, Failed, Disabled, No-DNS, Ineligible).

Domain table

One row per CGP domain. Columns: name, status pill (colour-coded), CGP-side CertificateType, issuer, fingerprint (truncated), days-to-expiry (red if past, amber if under threshold), attempt count, last error.

Per-row action buttons:

Recent log

Last 50 audit lines from the state DB: discover, renew-success, renew-failed, enable, disable, notification-sent.

Config panel

View + edit the TOML in-place. Save writes to the file on disk and reloads. Fields that would break the running scheduler (interval, threshold) apply immediately; changes to [cgp] host/port take effect on the next connection.

Section 8

Eligibility filter

The customer’s core requirement: most CGP domains should NOT get a public cert. This is what the filter does, per domain.

Partial-cert fallback: a domain with 3 aliases where 1 fails still gets a cert, covering the 2 that passed. The dropped name goes into the dropped array and (if [smtp.events].sans_shrunk = true) triggers a notification email at renewal time.

Auto-disable: when auto_disable = true, a domain that just flipped from Active/Pending to Ineligible has its CertificateType set to Disabled via UPDATEDOMAINSETTINGS. This is idempotent — the setting is checked first, only written if it differs. Operator-set Disabled is never overridden.

Section 9

Auto-renewal scheduler

When [renewal] enabled = true, a background tokio::time::interval task ticks every interval_seconds. First tick is skipped (avoids startup race). Each tick runs one renewal pass:

AttemptBackoff before next
0 → 11 hour
1 → 26 hours
2 → 324 hours
3 → 424 hours
4 → 5— flips to failed, needs manual reset
Section 10

Postmaster notifications

Four event classes, each independently toggleable. Every notification carries timestamp, domain, and error/context.

issuance_failed

The pipeline could not obtain a cert — ACME account creation failed, order was rejected, all SANs failed validation, LE returned a hard error, or install into CGP failed the read-back verify.

sans_shrunk

A renewal that yielded a cert covering fewer SAN names than the previous cert. Body lists which names were lost and which were kept. Operator investigates: did an alias’s DNS move? Was a domain removed? Did a CF token lose access to a zone?

no_dns

A configured domain or alias didn’t resolve at all (NXDOMAIN). Usually a stale DomainAliases entry.

not_local

Resolves but the IP isn’t in local_ips. Common cause: domain moved behind Cloudflare / another CDN, or your mailserver’s IP changed and local_ips is stale.

Section 11

Cloudflare DNS-01 setup

DNS-01 is the recommended path for any domain fronted by a CDN (which most CGP-served domains are). It doesn’t require the helper to serve HTTP on port 80.

Create the API token

Find the zone ID

Cloudflare dashboard → click your zone → the right sidebar shows Zone ID (32 hex chars).

Put both into [cloudflare]

[cloudflare]
api_token = "cfut_...your token..."
zone_id   = "6b57eb04d8a0ee5d819f69499186e300"

Restart cert-helper. That’s it. First Test renewal or Renew now will use it.

⚠
Single-zone scope today. If your CGP domains live in multiple Cloudflare zones (e.g. cgatepro.net and rhone.ch), you need one cert-helper instance per zone until multi-zone support lands.
Section 12

HTTP-01 alternative

When there’s no CDN in front of your domains, HTTP-01 is simpler: LE fetches http://<domain>/.well-known/acme-challenge/<token>, expects the key-authorization as the body.

Configure by omitting the [cloudflare] section and pointing [acme].http01_dir to a directory some HTTP server can serve at /.well-known/acme-challenge/. Two typical setups:

CGP’s own HTTP

CGP’s HTTPU already serves /webskins/<skin>/* paths. You can add a static mapping in the CGP Webskin config so /.well-known/acme-challenge/* serves from cert-helper’s http01_dir. Requires a small skin edit.

Nginx / Apache in front

Most reverse-proxy setups already do this:

location /.well-known/acme-challenge/ {
    root /var/lib/cert-helper;   # http01_dir’s parent
    default_type text/plain;
}
Section 13

Staging vs production

Let’s Encrypt runs two directories:

EnvironmentDirectory URLCert accepted by browsers?Rate limit
Staginghttps://acme-staging-v02.api.letsencrypt.org/directoryNo — issuer “(STAGING) Ersatz Emmer”~30k orders / 3h
Productionhttps://acme-v02.api.letsencrypt.org/directoryYes50 certs / week / registered domain

Always develop against staging. Rate limits there are generous enough that an accidental infinite loop won’t lock you out. When staging works end-to-end for at least one domain:

Section 14

Troubleshooting

SymptomCauseFix
Dashboard says license: degradedNo license.key next to binaryCopy the key in, restart. Beta mode still works until 2026-12-31.
[PROTECT] refusing to start [tampered]Binary was modified after being stampedRestore original binary; don’t re-run strip, don’t patch resources.
Discover returns 0 domainsWrong admin_user / admin_pass, or CGP CLI blockedCheck cert-helper.log for the USER/PASS reply codes.
Renew fails with NXDOMAIN … _acme-challenge.<domain>Wrong CF token scope or wrong zone; propagation delay longer than 45s (rare)Verify token with curl -H "Authorization: Bearer $TOKEN" https://api.cloudflare.com/client/v4/user/tokens/verify. Confirm zone_id matches. Retry manually.
Renew fails with rate limit exceededHit LE production rate limitSwitch back to staging until you understand the limit. Every failed order counts.
Cert installs but browsers show the old oneCGP caches active certs in memoryRestart CGP: sc restart CommuniGatePro (Windows) / systemctl restart CGPro (Linux).
Install returns 500 from CGP but fingerprint matchesKnown CGP quirk — 500 on successNon-issue. The read-back verify is authoritative.
Row stuck in failed after 5 attemptsAll backoff exhaustedClick Renew now on the row — resets counter and tries once more.
SMTP notify never arrives[smtp] enabled = false, or no localhost relayFlip enabled, check cert-helper.log for SMTP conversation lines.
Domain flipped from Active to Ineligible unexpectedlyDNS moved, or local_ips is staleRe-check the domain’s A record; update local_ips; re-discover.
Section 15

Uninstall

Windows

sc.exe stop cert-helper
cd C:\CommuniGate\Helpers\cert-helper
.\cert-helper.exe --uninstall-service
Remove-Item -Recurse C:\CommuniGate\Helpers\cert-helper

Linux

sudo systemctl stop cert-helper
sudo systemctl disable cert-helper
sudo rm /etc/systemd/system/cert-helper.service
sudo systemctl daemon-reload
sudo rm -r /opt/cert-helper /var/lib/cert-helper /var/log/cert-helper
⚠
Removing the helper doesn’t touch certs it installed. They stay in CGP until the next real renewal — which, without the helper, means manual admin work every 90 days.
Section 16

FAQ & glossary

Does cert-helper support wildcard certs?

Yes for issuance (LE supports *.example.com via DNS-01 only), but the current release doesn’t explicitly configure them. Add the wildcard as a DomainAliases entry on the CGP domain and it becomes a SAN candidate. Future release will add a first-class wildcard toggle.

Can I run one cert-helper for multiple CGP hosts?

Yes if they share a common CGP admin over a network-reachable port 106, but the recommended pattern is one cert-helper per CGP host — state stays local, dashboard is loopback-only, and there’s no cross-host coordination in the DB schema.

Does cert-helper store private keys?

Each issued cert’s private key is generated freshly per renewal, sent to CGP’s PrivateSecureKey field, and dropped from cert-helper memory. The state DB stores only the fingerprint and metadata, never the key material.

What happens if my Cloudflare API token is revoked?

Next renewal for any domain in that zone fails with an HTTP 401 from Cloudflare’s API. The issuance_failed notification fires. Rotate the token, edit [cloudflare].api_token, restart. Backoff timers reset on the next manual Renew.

Can I use another CA (ZeroSSL, Buypass) instead of Let’s Encrypt?

Any RFC 8555 ACME CA works — just point [acme].directory_url at their directory. Tested only against LE.

What’s the difference between failed and ineligible?

failed = we tried to renew and something went wrong (5 attempts exhausted). Row waits for operator action. ineligible = the eligibility filter said no (DNS, local-IP, or allow/deny gate). Row will be re-checked next discovery pass; if the underlying condition changes (new DNS, updated local_ips, adjusted allow-list), it’s promoted back to pending and picked up by the next scheduler tick.

What’s the license about?

An RSA-2048-signed JSON blob listing which helpers this box is licensed to run and until when. Sibling to notification-helper, dkim-signer, mailing-list-helper. cert-helper accepts any license whose Helpers array contains "cert-helper". Contact [email protected] to obtain one, or generate with the internal license-generator-v2.py.

Glossary

ACMEAutomated Certificate Management Environment, RFC 8555 — the protocol Let’s Encrypt speaks.
SANSubject Alternative Name — DNS name a cert is valid for. Modern certs carry all names in SANs; the legacy Common Name field is ignored.
DNS-01ACME challenge type where the ACME server verifies control of a domain by looking up a TXT record at _acme-challenge.<domain>.
HTTP-01ACME challenge type where the ACME server verifies control by GET-ing http://<domain>/.well-known/acme-challenge/<token>.
PKCS#1 / PKCS#8Two different PEM encodings for RSA private keys. CGP requires PKCS#1 (“traditional RSA”); modern ACME clients emit PKCS#8. cert-helper converts.
CSRCertificate Signing Request — a signed statement of intent submitted to the CA during finalize.
FingerprintSHA-256 hash over the certificate DER bytes. Used by the read-back verify to prove the cert CGP now serves is the one we just handed it.