Skip to content

refactor(api): extract tls provisioning into internal/api/tlsutil leaf (ADR-0011)#453

Merged
krisarmstrong merged 1 commit into
mainfrom
refactor/api-tls-leaf
Jun 16, 2026
Merged

refactor(api): extract tls provisioning into internal/api/tlsutil leaf (ADR-0011)#453
krisarmstrong merged 1 commit into
mainfrom
refactor/api-tls-leaf

Conversation

@krisarmstrong

Copy link
Copy Markdown
Collaborator

Summary

Extracts the TLS provisioning concern out of the flat internal/api
namespace into the internal/api/tlsutil leaf package — the third
slice of the ADR-0011 internal/api sub-package decomposition (after
ratelimit #451 and sse #452).

The leaf owns: the Config/ACMEConfig settings structs, ServerConfig
(the TLS 1.3 tls.Config template), EnsureSelfSignedCert (self-signed
cert generation), the ACME constructors (NewACMEManager,
ACMETLSConfig), and the FingerprintCache (active-cert SHA-256
fingerprint, surfaced via /__version). It depends only on stdlib,
golang.org/x/crypto (ACME), and internal/logging. The new
api-tlsutil-isolated depguard rule statically forbids any upward import
of the transport layer, so an accidental re-coupling fails CI.

The HTTP serving lifecycle stays in internal/api: startTLS /
startTLSWithACME (listener binding + the port-80 HTTP-01 challenge
server) and the two Server methods that bridge the cache into the
request path — activeCertPath and tlsFingerprintForResponse.

Drift fixed along the way: two constants sat in the old tls.go
const block but had nothing to do with TLS. Rather than drag them into
the leaf, they are rehomed to their real owners:

  • refreshMultiplier (auth refresh-cookie lifetime) → handlers_auth.go
  • acmeReadHeaderTimeoutSec (transport challenge-server timeout) → server.go

The *ForTest ACME wrappers in test_helpers.go are deleted; the TLS
tests now exercise the leaf's exported API directly from
internal/api/tlsutil.

No behaviour change: endpoints, the /__version fingerprint, and cert
paths are identical. Net effect: internal/api shrinks by two files and
one snake_case filename (tls_fingerprint.go); internal/api underscore
count 20 → 19.

Linked Issue

Related to #450

Testing Evidence

Gated from a clean worktree off origin/main (go 1.26.4,
golangci-lint v2.12.2):

$ go build ./...
FULL_BUILD_OK

$ go vet ./internal/api/...
VET_DONE

$ golangci-lint run ./internal/api/...
0 issues.

$ go test ./internal/api/... -count=1
ok  github.com/MustardSeedNetworks/stem/internal/api            228.778s
ok  github.com/MustardSeedNetworks/stem/internal/api/ratelimit  0.092s
ok  github.com/MustardSeedNetworks/stem/internal/api/sse        0.066s
ok  github.com/MustardSeedNetworks/stem/internal/api/tlsutil    5.564s

$ bash scripts/check-route-policy.sh
✓ Route-policy gate: all /api routes go through the capability registry.

$ bash scripts/check-output-escaping.sh
OK: no raw innerHTML injection and no value-interpolating Fprintf in internal/api.

$ bash scripts/check-json-casing.sh
JSON casing gate OK — no new snake_case wire tags.

$ STRICT=1 bash scripts/check-file-size.sh
📊 Found 0 red flag(s), 38 warning(s)   # warnings are pre-existing UI files

$ gitleaks protect --staged --no-banner
no leaks found

Security and Release Checklist

  • No behaviour change — pure structural move; endpoints, /__version
    fingerprint, and cert paths are identical.
  • Leaf boundary statically enforced (api-tlsutil-isolated depguard
    rule); golangci-lint clean.
  • No new secrets; gitleaks clean on staged content.
  • No new mutating routes / auth surfaces — security invariants
    (CSRF, rate-limit, role gating, output encoding) untouched.
  • TLS 1.3 minimum preserved for both self-signed and ACME paths;
    private-key file mode 0o600 preserved.
  • No customer-facing copy; no banned vocabulary.
  • ADR-0011 updated to record the tlsutil slice.

…f (ADR-0011)

Move certificate provisioning, ACME manager construction, the TLS 1.3
config template, and the active-cert fingerprint cache out of the flat
internal/api namespace into the internal/api/tlsutil leaf package
(ADR-0011). The leaf depends only on stdlib, golang.org/x/crypto, and
internal/logging; the api-tlsutil-isolated depguard rule statically
forbids any upward import of the transport layer.

The HTTP serving lifecycle stays in internal/api: startTLS /
startTLSWithACME and the two Server methods that bridge the cache to the
request path (activeCertPath, tlsFingerprintForResponse).

Two constants that lived in the old tls.go const block but are unrelated
to TLS are rehomed to the api layer rather than dragged into the leaf:
refreshMultiplier (auth cookie lifetime -> handlers_auth.go) and
acmeReadHeaderTimeoutSec (challenge-server timeout -> server.go). The
ForTest ACME wrappers in test_helpers.go are dropped; the TLS tests now
exercise the leaf's exported API directly from internal/api/tlsutil.

No behaviour change: endpoints, /__version fingerprint, and cert paths
are identical.
@github-actions

Copy link
Copy Markdown
Contributor

License Compliance Report

All dependencies pass license compliance checks

Go Dependencies

  • Unknown: 34 package(s)
  • MIT: 26 package(s)
  • BSD-3-Clause: 16 package(s)
  • Apache-2.0: 11 package(s)
  • BSD-2-Clause: 1 package(s)

npm Dependencies

See full report in workflow artifacts

Allowed Licenses: MIT, Apache-2.0, BSD-*, ISC, CC0-1.0, MPL-2.0
Forbidden: GPL, AGPL, SSPL (strong copyleft)

@krisarmstrong krisarmstrong merged commit 51dda73 into main Jun 16, 2026
27 checks passed
@krisarmstrong krisarmstrong deleted the refactor/api-tls-leaf branch June 16, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant