From a50e3d8291bbafb882e3540306aa71b101bc8838 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 23 Apr 2026 18:40:02 +0100 Subject: [PATCH] test(agentic): add HTTPS cert regression tests + fleet sync audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fleet registration in pkg/agentic already goes through the shared &http.Client{Timeout: 30s} at transport.go:13 — no InsecureSkipVerify, no custom TLS transport. This audit documents that finding and adds regression coverage so future refactors can't silently strip TLS validation from the /v1/fleet/register path. Verdict: OK. No production bug. Tests pass trusted TLS server case and reject untrusted cert with a wrapped error that surfaces the certificate / x509 / tls signal in the message. Closes tasks.lthn.sh/view.php?id=29 Co-authored-by: Codex Co-Authored-By: Virgil --- docs/audits/fleet-https-cert-20260423.md | 24 +++++++ pkg/agentic/platform_test.go | 82 ++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 docs/audits/fleet-https-cert-20260423.md diff --git a/docs/audits/fleet-https-cert-20260423.md b/docs/audits/fleet-https-cert-20260423.md new file mode 100644 index 0000000..ee64b1b --- /dev/null +++ b/docs/audits/fleet-https-cert-20260423.md @@ -0,0 +1,24 @@ +# Fleet HTTPS Certificate Audit - 2026-04-23 + +## Verdict + +**OK** + +Fleet registration already goes through a TLS-validating `http.Client`; no production code in `pkg/agentic` overrides TLS verification on the `/v1/fleet/register` path. The audit added regression coverage so this path now fails loudly if certificate verification is bypassed or broken. + +## What was checked + +- Fleet registration is implemented by `handleFleetRegister`, which builds the registration payload and posts it to `/v1/fleet/register` via `platformPayload` at `pkg/agentic/platform.go:199`, `pkg/agentic/platform.go:210`, and `pkg/agentic/platform.go:221`. +- `platformPayload` sends that request through `HTTPDo` with a Bearer token and the platform base URL from `syncAPIURL()` at `pkg/agentic/platform.go:558`, `pkg/agentic/platform.go:569`, and `pkg/agentic/sync.go:252`. +- `HTTPDo` delegates to `httpDo`, and `httpDo` executes the request with `defaultClient.Do(request)` at `pkg/agentic/transport.go:99`, `pkg/agentic/transport.go:139`, and `pkg/agentic/transport.go:161`. +- The only shared production client on this path is `defaultClient`, defined as `&http.Client{Timeout: 30 * time.Second}` with no custom transport or TLS override at `pkg/agentic/transport.go:13`. + +## Regression coverage added + +- `testDefaultClientWithTrustedServerCert` now builds a client that trusts only the test server certificate via `RootCAs`, and it explicitly asserts `InsecureSkipVerify` stays `false` at `pkg/agentic/platform_test.go:20` and `pkg/agentic/platform_test.go:28`. +- `TestPlatform_HandleFleetRegister_Good_TrustedTLS` proves the real fleet registration path succeeds against a TLS endpoint when the certificate is trusted by the client at `pkg/agentic/platform_test.go:104`, `pkg/agentic/platform_test.go:114`, and `pkg/agentic/platform_test.go:121`. +- `TestPlatform_HandleFleetRegister_Bad_UntrustedTLSCert` proves the same registration path rejects an untrusted certificate, never reaches the handler, and returns a wrapped error instead of succeeding silently at `pkg/agentic/platform_test.go:131`, `pkg/agentic/platform_test.go:144`, `pkg/agentic/platform_test.go:145`, and `pkg/agentic/platform_test.go:149`. + +## Test run + +- `go test -mod=mod ./pkg/agentic/...` passed in a temp workspace that preserved the repo's `../mcp` replace layout. diff --git a/pkg/agentic/platform_test.go b/pkg/agentic/platform_test.go index 9f713e2..cdc1155 100644 --- a/pkg/agentic/platform_test.go +++ b/pkg/agentic/platform_test.go @@ -4,8 +4,11 @@ package agentic import ( "context" + "crypto/tls" + "crypto/x509" "net/http" "net/http/httptest" + "sync/atomic" "testing" "time" @@ -14,6 +17,32 @@ import ( "github.com/stretchr/testify/require" ) +func testDefaultClientWithTrustedServerCert(t *testing.T, srv *httptest.Server) *http.Client { + t.Helper() + + roots := x509.NewCertPool() + roots.AddCert(srv.Certificate()) + + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{RootCAs: roots} + require.False(t, transport.TLSClientConfig.InsecureSkipVerify) + + return &http.Client{ + Timeout: defaultClient.Timeout, + Transport: transport, + } +} + +func testUseDefaultClient(t *testing.T, client *http.Client) { + t.Helper() + + original := defaultClient + defaultClient = client + t.Cleanup(func() { + defaultClient = original + }) +} + func testPrepWithPlatformServer(t *testing.T, srv *httptest.Server, token string) *PrepSubsystem { t.Helper() @@ -72,6 +101,59 @@ func TestPlatform_HandleFleetRegister_Good(t *testing.T) { assert.Nil(t, node.CurrentTaskID) } +func TestPlatform_HandleFleetRegister_Good_TrustedTLS(t *testing.T) { + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NotNil(t, r.TLS) + require.Equal(t, "/v1/fleet/register", r.URL.Path) + require.Equal(t, "Bearer secret-token", r.Header.Get("Authorization")) + + _, _ = w.Write([]byte(`{"data":{"id":2,"agent_id":"charon","platform":"linux","status":"online"}}`)) + })) + defer server.Close() + + testUseDefaultClient(t, testDefaultClientWithTrustedServerCert(t, server)) + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.handleFleetRegister(context.Background(), core.NewOptions( + core.Option{Key: "agent_id", Value: "charon"}, + core.Option{Key: "platform", Value: "linux"}, + )) + require.True(t, result.OK) + + node, ok := result.Value.(FleetNode) + require.True(t, ok) + assert.Equal(t, 2, node.ID) + assert.Equal(t, "charon", node.AgentID) + assert.Equal(t, "linux", node.Platform) + assert.Equal(t, "online", node.Status) +} + +func TestPlatform_HandleFleetRegister_Bad_UntrustedTLSCert(t *testing.T) { + var called atomic.Bool + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called.Store(true) + _, _ = w.Write([]byte(`{"data":{"id":3,"agent_id":"charon","platform":"linux","status":"online"}}`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.handleFleetRegister(context.Background(), core.NewOptions( + core.Option{Key: "agent_id", Value: "charon"}, + core.Option{Key: "platform", Value: "linux"}, + )) + require.False(t, result.OK) + assert.False(t, called.Load()) + + err, ok := result.Value.(error) + require.True(t, ok) + assert.Contains(t, err.Error(), "platform request failed") + assert.True(t, + core.Contains(err.Error(), "certificate") || + core.Contains(err.Error(), "x509") || + core.Contains(err.Error(), "tls"), + ) +} + func TestPlatform_HandleFleetHeartbeat_Good_ComputeBudget(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "/v1/fleet/heartbeat", r.URL.Path)