Closes Top-10 fix#1 of the 2026-05-03 issuer-coverage audit (see
cowork/issuer-coverage-audit-2026-05-03/RESULTS.md). Pre-fix,
ejbca.go::New called tls.LoadX509KeyPair once at construction and
configured the keypair into *http.Transport.TLSClientConfig with
no mtime watch. mTLS rotation required a server restart — quarterly
rotation per any reasonable security policy = quarterly deploy
outage.
Bundle M from the prior 2026-05-01 audit shipped the mtlscache
helper at internal/connector/issuer/mtlscache/cache.go and wired
it into Entrust + GlobalSign. EJBCA was missed in Bundle M's
scope. This commit ports the same helper onto EJBCA's
auth_mode=mtls path. The OAuth2 path is unchanged.
Implementation:
- New imports internal/connector/issuer/mtlscache.
- Connector struct gains an mtls *mtlscache.Cache field
(mirroring Entrust + GlobalSign).
- New()'s case 'mtls': replaces tls.LoadX509KeyPair + manual
*http.Transport with mtlscache.New(certPath, keyPath,
Options{HTTPTimeout: 30s}). Cache build happens at construction
so misconfigured operators fail fast (matches pre-fix
behaviour).
- New helper getHTTPClient() returns the cached client; on the
mTLS path it calls RefreshIfStale before returning so the
next request uses the new keypair if disk has rotated. On
OAuth2 / test paths (c.mtls == nil), returns c.httpClient
as-is.
- All 3 c.httpClient.Do call sites (IssueCertificate enroll,
RevokeCertificate revoke, GetOrderStatus cert lookup) replaced
with c.getHTTPClient() + client.Do.
- crypto/tls import removed (no longer used at this layer).
Tests:
- TestEJBCA_MTLSKeypairRotation_PicksUpNewCertWithoutRestart
(new, ejbca_mtls_rotation_test.go): generates two CAs (caA,
caB), signs leafA + leafB, spins up an httptest TLS server
that trusts both CAs and records the issuer DN of every
presented client cert, writes leafA, makes request 1, writes
leafB + advances mtime by 2s, makes request 2. Asserts the
server saw caA's DN on req 1 and caB's DN on req 2 — the
cache picked up the rotation without ejbca.New re-running.
- export_test.go: GetHTTPClientForTest helper exposes the
private getHTTPClient so the rotation test drives the
production code path.
- All existing EJBCA tests still pass (TestNew_MTLSWiresClientCert,
TestNew_MTLSCertLoadFailure, TestNew_OAuth2NoTransportTuning,
TestNew_InvalidAuthMode).
Verified locally:
- gofmt clean across the repo.
- go vet ./... clean across the repo.
- go test -race -count=1 -short ./internal/connector/issuer/ejbca/...
./internal/connector/issuer/mtlscache/... green.
Audit reference: cowork/issuer-coverage-audit-2026-05-03/
RESULTS.md Top-10 fix#1.