Skip to content

Commit 39ca58b

Browse files
[8.19] (backport #7804) [Integration Test] Ensure that upgrading a FIPS-capable Agent results in a FIPS-capable Agent (#8491)
* [Integration Test] Ensure that upgrading a FIPS-capable Agent results in a FIPS-capable Agent (#7804) * Adding skeleton for FIPS-to-FIPS upgrade test * Expose FIPS compliance in GRPC client Version call response * Test upgrade from FIPS to FIPS artifact * Change assert to require * Add postWatcherSuccessHook to upgrade test * Refactor standalone upgrade test to take upgradeOpts * Fix up FIPS upgrade test to use postWatcherSuccessHook to test FIPS compliance of upgraded Agent * Add version constraint to test * s/compliant/capable/ * s/compliant/capable/ * Append -fips to artifact name if current release of Agent is FIPS-capable * Enable FIPS-capable to FIPS-capable Agent upgrades * Running mage fmt * Adding test to ensure FIPS-capable Agent cannot be upgraded to non-FIPS-capable Agent * Adding return value * Fixing function calls * Remove post-upgrade success hook since we expect upgrade to fail * Add minimum FIPS version check for start version * Simplify upgradeOpts initialization * Add version equality comparison method * Fix version checks in tests * Refactor version check into own helper function * Fixing args * No need to pass testing.T * Remove redundant test case * Restrict FIPS integration tests to Linux * Add Fleet-managed Agent FIPS upgrade test * Remove integration test trying to upgrade FIPS to non-FIPS * Fix type of argument * Refactoring: extract common logic into helper function * Remove old code * Require no error * Fixing syntax errors * Define tests as needing a FIPS environment * FIPS upgrade tests should only run on Linux * FIPS upgrade tests should start with FIPS-capable version * Fixing comment + skip message * Fix syntax errors * Removing TestStandaloneUpgradeFIPStoFIPS test * Removing TestFleetManagedUpgradePrivilegedFIPS test * Add back accidentally-deleted function * Combine less and equal unit tests * Hash replaceToken only if its non-empty * Use startFixture (cherry picked from commit ac9ee9a) * Fixing conflicts --------- Co-authored-by: Shaunak Kashyap <[email protected]>
1 parent fcd98d7 commit 39ca58b

File tree

16 files changed

+299
-63
lines changed

16 files changed

+299
-63
lines changed

internal/pkg/agent/application/upgrade/step_unpack.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,10 @@ func readCommitHash(reader io.Reader) (string, error) {
419419
}
420420

421421
func getFileNamePrefix(archivePath string) string {
422-
return strings.TrimSuffix(filepath.Base(archivePath), ".tar.gz") + "/" // omitting `elastic-agent-{version}-{os}-{arch}/` in filename
422+
prefix := strings.TrimSuffix(filepath.Base(archivePath), ".tar.gz") + "/" // omitting `elastic-agent-{version}-{os}-{arch}/` in filename
423+
prefix = strings.Replace(prefix, fipsPrefix, "", 1)
424+
425+
return prefix
423426
}
424427

425428
func validFileName(p string) bool {

internal/pkg/agent/application/upgrade/step_unpack_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,27 @@ func addEntryToZipArchive(af files, writer *zip.Writer) error {
419419

420420
return nil
421421
}
422+
423+
func TestGetFileNamePrefix(t *testing.T) {
424+
tests := map[string]struct {
425+
archivePath string
426+
expectedPrefix string
427+
}{
428+
"fips": {
429+
archivePath: "/foo/bar/elastic-agent-fips-9.1.0-SNAPSHOT-linux-arm64.tar.gz",
430+
expectedPrefix: "elastic-agent-9.1.0-SNAPSHOT-linux-arm64/",
431+
},
432+
"no_fips": {
433+
archivePath: "/foo/bar/elastic-agent-9.1.0-SNAPSHOT-linux-arm64.tar.gz",
434+
expectedPrefix: "elastic-agent-9.1.0-SNAPSHOT-linux-arm64/",
435+
},
436+
}
437+
438+
for name, test := range tests {
439+
t.Run(name, func(t *testing.T) {
440+
prefix := getFileNamePrefix(test.archivePath)
441+
require.Equal(t, test.expectedPrefix, prefix)
442+
})
443+
}
444+
445+
}

internal/pkg/agent/application/upgrade/upgrade.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const (
4646
runDirMod = 0770
4747
snapshotSuffix = "-SNAPSHOT"
4848
watcherMaxWaitTime = 30 * time.Second
49+
fipsPrefix = "-fips"
4950
)
5051

5152
var agentArtifact = artifact.Artifact{
@@ -61,6 +62,12 @@ var (
6162
ErrFipsToNonFips = errors.New("cannot switch to non-fips mode when upgrading")
6263
)
6364

65+
func init() {
66+
if release.FIPSDistribution() {
67+
agentArtifact.Cmd += fipsPrefix
68+
}
69+
}
70+
6471
// Upgrader performs an upgrade
6572
type Upgrader struct {
6673
log *logger.Logger
@@ -174,10 +181,12 @@ func checkUpgrade(log *logger.Logger, currentVersion, newVersion agentVersion, m
174181
}
175182

176183
if currentVersion.fips && !metadata.manifest.Package.Fips {
184+
log.Warnf("Upgrade action skipped because FIPS-capable Agent cannot be upgraded to non-FIPS-capable Agent")
177185
return ErrFipsToNonFips
178186
}
179187

180188
if !currentVersion.fips && metadata.manifest.Package.Fips {
189+
log.Warnf("Upgrade action skipped because non-FIPS-capable Agent cannot be upgraded to FIPS-capable Agent")
181190
return ErrNonFipsToFips
182191
}
183192

internal/pkg/agent/cmd/enroll_cmd.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,10 +1054,18 @@ func createFleetConfigFromEnroll(accessAPIKey string, enrollmentToken string, re
10541054
if err != nil {
10551055
return nil, errors.New(err, "failed to generate enrollment hash", errors.TypeConfig)
10561056
}
1057-
cfg.ReplaceTokenHash, err = fleetHashToken(replaceToken)
1058-
if err != nil {
1059-
return nil, errors.New(err, "failed to generate replace token hash", errors.TypeConfig)
1057+
1058+
// Hash replaceToken if provided; it is not expected to be provided when an Agent
1059+
// is being enrolled for the very first time. Hashing an empty replaceToken with the
1060+
// FIPS-capable build of Elastic Agent results in an "invalid key length" error from
1061+
// OpenSSL's FIPS provider.
1062+
if replaceToken != "" {
1063+
cfg.ReplaceTokenHash, err = fleetHashToken(replaceToken)
1064+
if err != nil {
1065+
return nil, errors.New(err, "failed to generate replace token hash", errors.TypeConfig)
1066+
}
10601067
}
1068+
10611069
if err := cfg.Valid(); err != nil {
10621070
return nil, errors.New(err, "invalid enrollment options", errors.TypeConfig)
10631071
}

pkg/control/v2/client/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ func (c *client) Version(ctx context.Context) (Version, error) {
300300
Commit: res.Commit,
301301
BuildTime: bt,
302302
Snapshot: res.Snapshot,
303+
Fips: res.Fips,
303304
}, nil
304305
}
305306

pkg/testing/define/define.go

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"sync"
1919
"testing"
2020

21+
"github.com/stretchr/testify/require"
22+
2123
"github.com/gofrs/uuid/v5"
2224

2325
"github.com/elastic/elastic-agent-libs/kibana"
@@ -90,33 +92,31 @@ func Version() string {
9092
// NewFixtureFromLocalBuild returns a new Elastic Agent testing fixture with a LocalFetcher and
9193
// the agent logging to the test logger.
9294
func NewFixtureFromLocalBuild(t *testing.T, version string, opts ...atesting.FixtureOpt) (*atesting.Fixture, error) {
93-
buildsDir := os.Getenv("AGENT_BUILD_DIR")
94-
if buildsDir == "" {
95-
projectDir, err := findProjectRoot()
96-
if err != nil {
97-
return nil, err
98-
}
99-
buildsDir = filepath.Join(projectDir, "build", "distributions")
100-
}
101-
102-
return NewFixtureWithBinary(t, version, "elastic-agent", buildsDir, opts...)
95+
return NewFixtureWithBinary(t, version, "elastic-agent", buildsDir(t), false, opts...)
96+
}
10397

98+
// NewFixtureFromLocalFIPSBuild returns a new FIPS-capable Elastic Agent testing fixture with a LocalFetcher
99+
// and the agent logging to the test logger.
100+
func NewFixtureFromLocalFIPSBuild(t *testing.T, version string, opts ...atesting.FixtureOpt) (*atesting.Fixture, error) {
101+
return NewFixtureWithBinary(t, version, "elastic-agent", buildsDir(t), true, opts...)
104102
}
105103

106104
// NewFixtureWithBinary returns a new Elastic Agent testing fixture with a LocalFetcher and
107105
// the agent logging to the test logger.
108-
func NewFixtureWithBinary(t *testing.T, version string, binary string, buildsDir string, opts ...atesting.FixtureOpt) (*atesting.Fixture, error) {
106+
func NewFixtureWithBinary(t *testing.T, version string, binary string, buildsDir string, fips bool, opts ...atesting.FixtureOpt) (*atesting.Fixture, error) {
109107
ver, err := semver.ParseVersion(version)
110108
if err != nil {
111109
return nil, fmt.Errorf("%q is an invalid agent version: %w", version, err)
112110
}
113111

114-
var binFetcher atesting.Fetcher
112+
localFetcherOpts := []atesting.LocalFetcherOpt{atesting.WithCustomBinaryName(binary)}
115113
if ver.IsSnapshot() {
116-
binFetcher = atesting.LocalFetcher(buildsDir, atesting.WithLocalSnapshotOnly(), atesting.WithCustomBinaryName(binary))
117-
} else {
118-
binFetcher = atesting.LocalFetcher(buildsDir, atesting.WithCustomBinaryName(binary))
114+
localFetcherOpts = append(localFetcherOpts, atesting.WithLocalSnapshotOnly())
119115
}
116+
if fips {
117+
localFetcherOpts = append(localFetcherOpts, atesting.WithLocalFIPSOnly())
118+
}
119+
binFetcher := atesting.LocalFetcher(buildsDir, localFetcherOpts...)
120120

121121
opts = append(opts, atesting.WithFetcher(binFetcher), atesting.WithLogOutput())
122122
if binary != "elastic-agent" {
@@ -308,3 +308,16 @@ func getKibanaClient() (*kibana.Client, error) {
308308
}
309309
return c, nil
310310
}
311+
312+
func buildsDir(t *testing.T) string {
313+
t.Helper()
314+
315+
buildsDir := os.Getenv("AGENT_BUILD_DIR")
316+
if buildsDir == "" {
317+
projectDir, err := findProjectRoot()
318+
require.NoError(t, err)
319+
buildsDir = filepath.Join(projectDir, "build", "distributions")
320+
}
321+
322+
return buildsDir
323+
}

pkg/testing/fetcher_local.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,35 @@ import (
1818
type localFetcher struct {
1919
dir string
2020
snapshotOnly bool
21+
fipsOnly bool
2122
binaryName string
2223
}
2324

24-
type localFetcherOpt func(f *localFetcher)
25+
type LocalFetcherOpt func(f *localFetcher)
2526

2627
// WithLocalSnapshotOnly sets the LocalFetcher to only pull the snapshot build.
27-
func WithLocalSnapshotOnly() localFetcherOpt {
28+
func WithLocalSnapshotOnly() LocalFetcherOpt {
2829
return func(f *localFetcher) {
2930
f.snapshotOnly = true
3031
}
3132
}
3233

34+
// WithLocalFIPSOnly sets the LocalFetcher to only pull a FIPS-compliant build.
35+
func WithLocalFIPSOnly() LocalFetcherOpt {
36+
return func(f *localFetcher) {
37+
f.fipsOnly = true
38+
}
39+
}
40+
3341
// WithCustomBinaryName sets the binary to a custom name, the default is `elastic-agent`
34-
func WithCustomBinaryName(name string) localFetcherOpt {
42+
func WithCustomBinaryName(name string) LocalFetcherOpt {
3543
return func(f *localFetcher) {
3644
f.binaryName = name
3745
}
3846
}
3947

4048
// LocalFetcher returns a fetcher that pulls the binary of the Elastic Agent from a local location.
41-
func LocalFetcher(dir string, opts ...localFetcherOpt) Fetcher {
49+
func LocalFetcher(dir string, opts ...LocalFetcherOpt) Fetcher {
4250
f := &localFetcher{
4351
dir: dir,
4452
binaryName: "elastic-agent",
@@ -56,6 +64,7 @@ func (f *localFetcher) Name() string {
5664

5765
// Fetch fetches the Elastic Agent and places the resulting binary at the path.
5866
func (f *localFetcher) Fetch(_ context.Context, operatingSystem string, architecture string, version string, packageFormat string) (FetcherResult, error) {
67+
prefix := GetPackagePrefix(f.fipsOnly)
5968
suffix, err := GetPackageSuffix(operatingSystem, architecture, packageFormat)
6069
if err != nil {
6170
return nil, err
@@ -66,7 +75,7 @@ func (f *localFetcher) Fetch(_ context.Context, operatingSystem string, architec
6675
return nil, fmt.Errorf("invalid version: %q: %w", ver, err)
6776
}
6877

69-
mainBuildfmt := "%s-%s-%s"
78+
mainBuildfmt := "%s-%s%s-%s"
7079
if f.snapshotOnly && !ver.IsSnapshot() {
7180
if ver.Prerelease() == "" {
7281
ver = semver.NewParsedSemVer(ver.Major(), ver.Minor(), ver.Patch(), "SNAPSHOT", ver.BuildMetadata())
@@ -85,10 +94,10 @@ func (f *localFetcher) Fetch(_ context.Context, operatingSystem string, architec
8594
}
8695

8796
if ver.IsSnapshot() && !matchesEarlyReleaseVersion {
88-
build := fmt.Sprintf(mainBuildfmt, f.binaryName, ver.VersionWithPrerelease(), suffix)
97+
build := fmt.Sprintf(mainBuildfmt, f.binaryName, prefix, ver.VersionWithPrerelease(), suffix)
8998
buildPath = filepath.Join(ver.BuildMetadata(), build)
9099
} else {
91-
buildPath = fmt.Sprintf(mainBuildfmt, f.binaryName, ver.String(), suffix)
100+
buildPath = fmt.Sprintf(mainBuildfmt, f.binaryName, prefix, ver.String(), suffix)
92101
}
93102

94103
fullPath := filepath.Join(f.dir, buildPath)

pkg/testing/fetcher_local_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func TestLocalFetcher(t *testing.T) {
5757
tcs := []struct {
5858
name string
5959
version string
60-
opts []localFetcherOpt
60+
opts []LocalFetcherOpt
6161
want []byte
6262
wantHash []byte
6363
}{
@@ -69,7 +69,7 @@ func TestLocalFetcher(t *testing.T) {
6969
}, {
7070
name: "SnapshotOnly",
7171
version: baseVersion,
72-
opts: []localFetcherOpt{WithLocalSnapshotOnly()},
72+
opts: []LocalFetcherOpt{WithLocalSnapshotOnly()},
7373
want: snapshotContent,
7474
wantHash: snapshotContentHash,
7575
}, {
@@ -80,13 +80,13 @@ func TestLocalFetcher(t *testing.T) {
8080
}, {
8181
name: "version with snapshot and SnapshotOnly",
8282
version: baseVersion + "-SNAPSHOT",
83-
opts: []localFetcherOpt{WithLocalSnapshotOnly()},
83+
opts: []LocalFetcherOpt{WithLocalSnapshotOnly()},
8484
want: snapshotContent,
8585
wantHash: snapshotContentHash,
8686
}, {
8787
name: "version with snapshot and build ID",
8888
version: baseVersion + "-SNAPSHOT+l5snflwr",
89-
opts: []localFetcherOpt{},
89+
opts: []LocalFetcherOpt{},
9090
want: snapshotContent,
9191
wantHash: snapshotContentHash,
9292
},

pkg/testing/fixture.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type Fixture struct {
5050
binaryName string
5151
runLength time.Duration
5252
additionalArgs []string
53+
fipsArtifact bool
5354

5455
srcPackage string
5556
workDir string
@@ -145,6 +146,12 @@ func WithAdditionalArgs(args []string) FixtureOpt {
145146
}
146147
}
147148

149+
func WithFIPSArtifact() FixtureOpt {
150+
return func(f *Fixture) {
151+
f.fipsArtifact = true
152+
}
153+
}
154+
148155
// NewFixture creates a new fixture to setup and manage Elastic Agent.
149156
func NewFixture(t *testing.T, version string, opts ...FixtureOpt) (*Fixture, error) {
150157
// we store the caller so the fixture can find the cache directory for the artifacts that

pkg/version/version_parser.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ func (psv ParsedSemVer) IndependentBuildID() string {
144144
return ""
145145
}
146146

147+
func (psv ParsedSemVer) Equal(other ParsedSemVer) bool {
148+
return !psv.Less(other) && !other.Less(psv)
149+
}
150+
147151
func (psv ParsedSemVer) Less(other ParsedSemVer) bool {
148152
// compare major version
149153
if psv.major != other.major {

0 commit comments

Comments
 (0)