test(agentic): add HTTPS cert regression tests + fleet sync audit
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 <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
9a90b9a651
commit
a50e3d8291
2 changed files with 106 additions and 0 deletions
24
docs/audits/fleet-https-cert-20260423.md
Normal file
24
docs/audits/fleet-https-cert-20260423.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue