From 1cb6d1a148190fb8af161338d04f9e0ef767a152 Mon Sep 17 00:00:00 2001 From: Dominik Rosiek Date: Fri, 17 Jul 2026 15:28:21 +0200 Subject: [PATCH] feat(fips): add per-module FIPS compliance gate and update elastic-agent-libs - Add fips_compliance_test.go to each Go module (cmd/fleet, pkg/api, testing/e2e): uses fipsscan.CheckViolations (from elastic-agent-libs) to assert no transitive non-FIPS crypto imports when built with the requirefips tag. - Add mage check:FIPSComplianceGate target that enforces every module in the repo (excluding dev-tools) carries a fips_compliance_test.go, preventing new modules from silently bypassing the gate. - Upgrade elastic-agent-libs to v0.46.2-0.20260717072702-02294d812c7d across all go.mod files; add it as a direct dependency of pkg/api (required by fipsscan). - Fix instrumentation.go: pass explicit logger to tlscommon.LoadCertificateAuthorities, which now requires one after the global-logger removal in elastic-agent-libs v0.46.1. Co-Authored-By: Claude Sonnet 4.6 --- cmd/fleet/fips_compliance_test.go | 52 ++++++++++++++++++++++++ go.mod | 2 +- go.sum | 2 + internal/pkg/config/instrumentation.go | 3 +- magefile.go | 55 ++++++++++++++++++++++++++ pkg/api/fips_compliance_test.go | 52 ++++++++++++++++++++++++ pkg/api/go.mod | 5 ++- pkg/api/go.sum | 2 + testing/e2e/fips_compliance_test.go | 52 ++++++++++++++++++++++++ testing/go.mod | 2 +- testing/go.sum | 4 +- 11 files changed, 225 insertions(+), 6 deletions(-) create mode 100644 cmd/fleet/fips_compliance_test.go create mode 100644 pkg/api/fips_compliance_test.go create mode 100644 testing/e2e/fips_compliance_test.go diff --git a/cmd/fleet/fips_compliance_test.go b/cmd/fleet/fips_compliance_test.go new file mode 100644 index 0000000000..283c3f0dc2 --- /dev/null +++ b/cmd/fleet/fips_compliance_test.go @@ -0,0 +1,52 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +//go:build requirefips + +package fleet_test + +import ( + "os/exec" + "strings" + "testing" + + "github.com/elastic/elastic-agent-libs/testing/fipsscan" +) + +// knownViolations maps component import paths to the reason they import a +// forbidden crypto library. An empty map is a strict gate: any new violation +// fails the test. Keys are the first-hop import from the binary package. +var knownViolations = map[string]string{} + +// TestFIPSFullyCompliant discovers every package main in this module and +// verifies that none of its transitive dependencies import a forbidden +// (non-FIPS) crypto library. A new binary added to the module is automatically +// covered without any changes to this file. +func TestFIPSFullyCompliant(t *testing.T) { + modOut, err := exec.CommandContext(t.Context(), "go", "list", "-m").Output() + if err != nil { + t.Fatalf("go list -m: %v", err) + } + module := strings.TrimSpace(string(modOut)) + + out, err := exec.CommandContext(t.Context(), "go", "list", + "-tags", "requirefips", + "-f", `{{if eq .Name "main"}}{{.ImportPath}}{{end}}`, + module+"/...", + ).Output() + if err != nil { + t.Fatalf("go list %s/...: %v", module, err) + } + + binaries := strings.Fields(string(out)) + if len(binaries) == 0 { + t.Fatalf("no package main found in module %s — update this test or add a binary", module) + } + + for _, bin := range binaries { + t.Run(bin, func(t *testing.T) { + fipsscan.CheckViolations(t, bin, bin, nil, knownViolations) + }) + } +} diff --git a/go.mod b/go.mod index 54fd8ac241..c8db0ebae5 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/dgraph-io/ristretto v0.2.0 github.com/docker/go-units v0.5.0 github.com/elastic/elastic-agent-client/v7 v7.18.1 - github.com/elastic/elastic-agent-libs v0.46.0 + github.com/elastic/elastic-agent-libs v0.46.2-0.20260717072702-02294d812c7d github.com/elastic/elastic-agent-system-metrics v0.14.4 github.com/elastic/go-elasticsearch/v8 v8.19.6 github.com/elastic/go-ucfg v0.9.1 diff --git a/go.sum b/go.sum index a6dc43d728..ad727f1dc5 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ github.com/elastic/elastic-agent-client/v7 v7.18.1 h1:WnM53JjaukeysrAuiTyrhDPmFx github.com/elastic/elastic-agent-client/v7 v7.18.1/go.mod h1:uDpSGZ+YCKgqgtkwCA0qjwX0gU/wmixDsVbPjY3GkPs= github.com/elastic/elastic-agent-libs v0.46.0 h1:4otnN3M34k2sngeeeSEza1YS+XKY1zZ8sBoSVhu2Tew= github.com/elastic/elastic-agent-libs v0.46.0/go.mod h1:axkpqDCCzAky6G4D/cklgtZQa3jTNGAMVHVkU0u6YbU= +github.com/elastic/elastic-agent-libs v0.46.2-0.20260717072702-02294d812c7d h1:JsN2CpGeT4ozSz0Cc6Gzo9YyOH9kt0y+y2yUXcQdzEc= +github.com/elastic/elastic-agent-libs v0.46.2-0.20260717072702-02294d812c7d/go.mod h1:axkpqDCCzAky6G4D/cklgtZQa3jTNGAMVHVkU0u6YbU= github.com/elastic/elastic-agent-system-metrics v0.14.4 h1:XGGepNVOxhtfJmanQsgqCAZESIJtyDIFOKErOaVzFEw= github.com/elastic/elastic-agent-system-metrics v0.14.4/go.mod h1:NyNMrdqMfznb/Zy8EmIAHlJpX6HuRlNW+bBL9UN7WQY= github.com/elastic/elastic-transport-go/v8 v8.9.0 h1:KeT/2P54F0xS0S8Y3Pf+tFDg4HmBgReQMB+BMz8dDAs= diff --git a/internal/pkg/config/instrumentation.go b/internal/pkg/config/instrumentation.go index 1f0b1ff769..a6e00fa7dd 100644 --- a/internal/pkg/config/instrumentation.go +++ b/internal/pkg/config/instrumentation.go @@ -13,6 +13,7 @@ import ( "net/url" "os" + "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent-libs/transport/tlscommon" apmtransport "go.elastic.co/apm/v2/transport" @@ -81,7 +82,7 @@ func (c *Instrumentation) APMHTTPTransportOptions() (apmtransport.HTTPTransportO } if c.TLS.ServerCA != "" { - pool, errs := tlscommon.LoadCertificateAuthorities([]string{c.TLS.ServerCA}) + pool, errs := tlscommon.LoadCertificateAuthorities([]string{c.TLS.ServerCA}, logp.NewLogger("config")) // FIXME once we update elastic-agent-libs to go 1.20 we can return multiple errors directly with errors.Join() if len(errs) != 0 { return apmtransport.HTTPTransportOptions{}, fmt.Errorf("unable to load instrumentation cas: %w", errors.Join(errs...)) diff --git a/magefile.go b/magefile.go index 02c5f7a2fc..b67e28ec9c 100644 --- a/magefile.go +++ b/magefile.go @@ -497,6 +497,61 @@ func (Check) DetectFIPSCryptoImports() error { return err } +// FIPSComplianceGate checks that every Go module in the repository (except +// tool-only modules with no Go packages) contains a fips_compliance_test.go file. +// Fails if any module is missing the file, preventing new modules from silently +// escaping the golang.org/x/crypto compliance gate. +func (Check) FIPSComplianceGate() error { + // Modules with no Go packages of their own (only tool directives). + skipModules := map[string]struct{}{ + "dev-tools": {}, + } + + var moduleRoots []string + if err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() && d.Name() == "go.mod" { + dir := filepath.Dir(path) + if _, skip := skipModules[filepath.Base(dir)]; !skip { + moduleRoots = append(moduleRoots, dir) + } + } + return nil + }); err != nil { + return fmt.Errorf("walking repo: %w", err) + } + + var missing []string + for _, root := range moduleRoots { + found := false + _ = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil || found { + return err + } + if d.IsDir() && path != root { + if _, statErr := os.Stat(filepath.Join(path, "go.mod")); statErr == nil { + return filepath.SkipDir + } + } + if !d.IsDir() && d.Name() == "fips_compliance_test.go" { + found = true + } + return nil + }) + if !found { + missing = append(missing, root) + } + } + + if len(missing) > 0 { + return fmt.Errorf("modules missing fips_compliance_test.go (add one per the FIPS compliance conventions):\n %s", + strings.Join(missing, "\n ")) + } + return nil +} + // genNotice generates the NOTICE.txt or the NOTICE-fips.txt file. func genNotice(fips bool) error { tags := []string{} diff --git a/pkg/api/fips_compliance_test.go b/pkg/api/fips_compliance_test.go new file mode 100644 index 0000000000..2e3bf487f6 --- /dev/null +++ b/pkg/api/fips_compliance_test.go @@ -0,0 +1,52 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +//go:build requirefips + +package api_testrt + +import ( + "os/exec" + "strings" + "testing" + + "github.com/elastic/elastic-agent-libs/testing/fipsscan" +) + +// knownViolations maps component import paths to the reason they import a +// forbidden crypto library. An empty map is a strict gate: any new violation +// fails the test. Keys are the first-hop import from the binary package. +var knownViolations = map[string]string{} + +// TestFIPSFullyCompliant discovers every package main in this module and +// verifies that none of its transitive dependencies import a forbidden +// (non-FIPS) crypto library. A new binary added to the module is automatically +// covered without any changes to this file. +func TestFIPSFullyCompliant(t *testing.T) { + modOut, err := exec.CommandContext(t.Context(), "go", "list", "-m").Output() + if err != nil { + t.Fatalf("go list -m: %v", err) + } + module := strings.TrimSpace(string(modOut)) + + out, err := exec.CommandContext(t.Context(), "go", "list", + "-tags", "requirefips", + "-f", `{{if eq .Name "main"}}{{.ImportPath}}{{end}}`, + module+"/...", + ).Output() + if err != nil { + t.Fatalf("go list %s/...: %v", module, err) + } + + binaries := strings.Fields(string(out)) + if len(binaries) == 0 { + t.Skip("no package main in module — library-only module, nothing to scan") + } + + for _, bin := range binaries { + t.Run(bin, func(t *testing.T) { + fipsscan.CheckViolations(t, bin, bin, nil, knownViolations) + }) + } +} diff --git a/pkg/api/go.mod b/pkg/api/go.mod index eea6518d6c..fe640110a4 100644 --- a/pkg/api/go.mod +++ b/pkg/api/go.mod @@ -2,7 +2,10 @@ module github.com/elastic/fleet-server/pkg/api go 1.25.0 -require github.com/oapi-codegen/runtime v1.5.0 +require ( + github.com/elastic/elastic-agent-libs v0.46.2-0.20260717072702-02294d812c7d + github.com/oapi-codegen/runtime v1.5.0 +) require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect diff --git a/pkg/api/go.sum b/pkg/api/go.sum index fba69b382b..b6e98e4b0d 100644 --- a/pkg/api/go.sum +++ b/pkg/api/go.sum @@ -5,6 +5,8 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elastic/elastic-agent-libs v0.46.2-0.20260717072702-02294d812c7d h1:JsN2CpGeT4ozSz0Cc6Gzo9YyOH9kt0y+y2yUXcQdzEc= +github.com/elastic/elastic-agent-libs v0.46.2-0.20260717072702-02294d812c7d/go.mod h1:axkpqDCCzAky6G4D/cklgtZQa3jTNGAMVHVkU0u6YbU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= diff --git a/testing/e2e/fips_compliance_test.go b/testing/e2e/fips_compliance_test.go new file mode 100644 index 0000000000..fb7f0dc997 --- /dev/null +++ b/testing/e2e/fips_compliance_test.go @@ -0,0 +1,52 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +//go:build requirefips + +package e2e_test + +import ( + "os/exec" + "strings" + "testing" + + "github.com/elastic/elastic-agent-libs/testing/fipsscan" +) + +// knownViolations maps component import paths to the reason they import a +// forbidden crypto library. An empty map is a strict gate: any new violation +// fails the test. Keys are the first-hop import from the binary package. +var knownViolations = map[string]string{} + +// TestFIPSFullyCompliant discovers every package main in this module and +// verifies that none of its transitive dependencies import a forbidden +// (non-FIPS) crypto library. A new binary added to the module is automatically +// covered without any changes to this file. +func TestFIPSFullyCompliant(t *testing.T) { + modOut, err := exec.CommandContext(t.Context(), "go", "list", "-m").Output() + if err != nil { + t.Fatalf("go list -m: %v", err) + } + module := strings.TrimSpace(string(modOut)) + + out, err := exec.CommandContext(t.Context(), "go", "list", + "-tags", "requirefips", + "-f", `{{if eq .Name "main"}}{{.ImportPath}}{{end}}`, + module+"/...", + ).Output() + if err != nil { + t.Fatalf("go list %s/...: %v", module, err) + } + + binaries := strings.Fields(string(out)) + if len(binaries) == 0 { + t.Skip("no package main in module — library-only module, nothing to scan") + } + + for _, bin := range binaries { + t.Run(bin, func(t *testing.T) { + fipsscan.CheckViolations(t, bin, bin, nil, knownViolations) + }) + } +} diff --git a/testing/go.mod b/testing/go.mod index 9f94fa068f..9c65a7e621 100644 --- a/testing/go.mod +++ b/testing/go.mod @@ -10,7 +10,7 @@ replace ( require ( github.com/Shopify/toxiproxy/v2 v2.12.0 github.com/elastic/elastic-agent-client/v7 v7.18.1 - github.com/elastic/elastic-agent-libs v0.46.1 + github.com/elastic/elastic-agent-libs v0.46.2-0.20260717072702-02294d812c7d github.com/elastic/fleet-server/pkg/api v0.0.0-00010101000000-000000000000 github.com/elastic/fleet-server/v7 v7.0.0-00010101000000-000000000000 github.com/gofrs/uuid/v5 v5.4.0 diff --git a/testing/go.sum b/testing/go.sum index 09cd0d2298..bae53a8654 100644 --- a/testing/go.sum +++ b/testing/go.sum @@ -45,8 +45,8 @@ github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elastic/elastic-agent-client/v7 v7.18.1 h1:WnM53JjaukeysrAuiTyrhDPmFxJG07ZAByc2TrkcKcs= github.com/elastic/elastic-agent-client/v7 v7.18.1/go.mod h1:uDpSGZ+YCKgqgtkwCA0qjwX0gU/wmixDsVbPjY3GkPs= -github.com/elastic/elastic-agent-libs v0.46.1 h1:1dVRKOaGWBxqDQPb/NCtoJHI8Nx3QbJOmr4v6y+UdKQ= -github.com/elastic/elastic-agent-libs v0.46.1/go.mod h1:axkpqDCCzAky6G4D/cklgtZQa3jTNGAMVHVkU0u6YbU= +github.com/elastic/elastic-agent-libs v0.46.2-0.20260717072702-02294d812c7d h1:JsN2CpGeT4ozSz0Cc6Gzo9YyOH9kt0y+y2yUXcQdzEc= +github.com/elastic/elastic-agent-libs v0.46.2-0.20260717072702-02294d812c7d/go.mod h1:axkpqDCCzAky6G4D/cklgtZQa3jTNGAMVHVkU0u6YbU= github.com/elastic/go-sysinfo v1.15.1 h1:zBmTnFEXxIQ3iwcQuk7MzaUotmKRp3OabbbWM8TdzIQ= github.com/elastic/go-sysinfo v1.15.1/go.mod h1:jPSuTgXG+dhhh0GKIyI2Cso+w5lPJ5PvVqKlL8LV/Hk= github.com/elastic/go-ucfg v0.9.1 h1:OwbVLC9pAmHqlBDq5owRC7HbfldsLuqPqrwg23n17BQ=