ADR-011: Three-mode TLS certificate management (off / manual / acme / self-signed)¶
Status: accepted
Context¶
Phase 8 introduces automated TLS certificate management. The research (2026-03-03) uncovered two findings that shape the design:
Finding 1 — CEPA uses HTTP only (critical protocol constraint)¶
The Dell PowerStore CEPA client sends events over plain HTTP only. All CEPA
integration documentation specifies http:// endpoint URLs; no HTTPS variant exists.
Dell's own documentation states: "events are forwarded to the CEPA server over an
HTTP connection."
Consequence: TLS on the cee-exporter CEPA listener (port 12228) does not
encrypt the PowerStore-to-exporter event channel. TLS on this port is useful only
when a TLS-capable reverse proxy terminates CEPA traffic, or if a future PowerStore
firmware version adds HTTPS receiver support. The existing ListenConfig.TLS
infrastructure is kept for these cases, but operators must understand the limitation.
Finding 2 — Manual certificate management is operationally brittle¶
The v1 TLS model requires operators to hand-manage certificate files and renewals. For deployments that do use TLS (e.g., reverse proxy scenarios or future CEPA HTTPS support), automated renewal via ACME is the industry standard.
Options evaluated¶
| Option | Summary | Verdict |
|---|---|---|
| Manual cert files only (v1 approach) | Operator manages rotation | KEPT as tls_mode="manual" for backward compat |
golang.org/x/crypto/acme/autocert |
In-process ACME, HTTP-01 or TLS-ALPN-01 | SELECTED for tls_mode="acme" |
go-acme/lego with DNS-01 |
Supports air-gapped/private networks | DEFERRED — adds 160+ DNS provider deps |
| External reverse proxy (Caddy/Traefik) | Proxy handles ACME automatically | Valid operational pattern; not built into daemon |
| Runtime self-signed generation | stdlib crypto/ecdsa + crypto/x509 |
SELECTED for tls_mode="self-signed" |
Rejected: DNS-01 via lego — the dependency weight (~180 DNS provider adapters) is
disproportionate. Air-gapped deployments are better served by self-signed mode or an
operator-managed CA.
Decision¶
Extend ListenConfig with a tls_mode field controlling four modes:
tls_mode |
Description |
|---|---|
"off" |
No TLS (default). Suitable for HTTP-only CEPA deployments. |
"manual" |
Operator supplies cert_file + key_file. Preserves v1 behaviour. |
"acme" |
autocert.Manager with DirCache, HostWhitelist, and a separate port-443 ACME challenge listener. Requires acme_domains and internet access to Let's Encrypt. |
"self-signed" |
Runtime ECDSA-P256 certificate generated via stdlib. No network needed. Valid for air-gapped or testing. |
Backward compatibility: existing configs with tls = true are migrated to
tls_mode = "manual" automatically at startup.
Library: golang.org/x/crypto/acme/autocert — already an indirect dependency via
x/sys. Promoting to direct dep. CGO-free. No new C toolchain requirement.
Self-signed generation: stdlib only — crypto/ecdsa, crypto/x509, crypto/rand,
encoding/pem. No external tools or openssl subprocess.
ACME challenge listener: TLS-ALPN-01 via a dedicated net.Listen(":443") goroutine,
separate from the CEPA listener on 12228. Port 443 binding requires AmbientCapabilities=
CAP_NET_BIND_SERVICE in the systemd unit.
Cache: autocert.DirCache("/var/cache/cee-exporter/acme") — operators using Docker
must mount this path as a persistent volume to avoid hitting Let's Encrypt rate limits
(5 certs / same identifier set / 7 days) on container restarts.
Consequences¶
golang.org/x/cryptois promoted from indirect to direct dep ingo.mod.tls_mode = "off"is the new default — existing deployments that relied on the implicit no-TLS behaviour are unaffected.- Operators who relied on the v1
tls = trueflag must migrate totls_mode = "manual"; a deprecation warning is logged when the old field is detected. - ACME mode requires port 443 to be reachable from Let's Encrypt servers. The systemd
unit must include
AmbientCapabilities=CAP_NET_BIND_SERVICE. - The Let's Encrypt staging URL must be used during development (
acme_staging = trueconfig option) to avoid the 5-cert-per-7-days rate limit.
Correction (2026-08-08):
acme_stagingwas never implemented — no such field exists inListenConfig, and this document is not being revised to remove the claim above because ADRs record the decision as made, not as later corrected. There is no staging toggle. For development, usetls_mode = "self-signed"instead; it needs no network access and sidesteps the rate limit entirely. - Self-signed mode produces browser-untrusted certificates (expected) — suitable for machine-to-machine scenarios where the CA can be added to the trust store. - TLS on port 12228 with CEPA source: clearly documented that this does NOT encrypt the PowerStore → cee-exporter path with current CEPA protocol versions.