Repository: elastic/elastic-agent
Pull request #7474: Switch to NewGCMWithRandomNonce for fips
Author: login: simitt
Labels: name: Team:Elastic-Agent-Control-Plane
description: Label for the Agent Control Plane team
name: skip-changelog
name: backport-8.x
description: Automated backport to the 8.x branch with mergify
Created: 2025-03-19T17:23:47Z | Merged: 2025-03-25T20:01:02Z

<!-- Type of change
Please label this PR with one of the following labels, depending on the scope of your change:
- Bug
- Enhancement
- Breaking change
- Deprecation
- Cleanup
- Docs
-->

## What does this PR do?

Replace `cipher.NewGCM` with `cipher.NewGCMWithRandomNonce` for fips mode and handle related code about not writing the `nonce` to the block anymore. 

<!-- Mandatory
Explain here the changes you made on the PR. Please explain the WHAT: patterns used, algorithms implemented, design architecture, message processing, etc.
-->

## Why is it important?
Remove findings from running with `GODEBUG=fips140=only`. 
<!-- Mandatory
Explain here the WHY, or the rationale/motivation for the changes.
-->

## Checklist

<!-- Mandatory
Add a checklist of things that are required to be reviewed in order to have the PR approved

List here all the items you have verified BEFORE sending this PR. Please DO NOT remove any item, striking through those that do not apply. (Just in case, strikethrough uses two tildes. ~~Scratch this.~~)
-->

- [ ] I have read and understood the [pull request guidelines](https://github.com/elastic/elastic-agent/blob/main/CONTRIBUTING.md#pull-request-guidelines) of this project.
- [ ] My code follows the style guidelines of this project
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] I have made corresponding change to the default configuration files
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] I have added an entry in `./changelog/fragments` using the [changelog tool](https://github.com/elastic/elastic-agent#changelog)
- [ ] I have added an integration test or an E2E test

## Disruptive User Impact

<!--
Will the changes introduced by this PR cause disruption to users in any way? If so, please describe what changes users
could make on their end to nullify or minimize this disruption. Consider impacts in related systems, not just directly
when using Elastic Agent.
-->

## How to test this PR locally

<!-- Recommended
Explain here how this PR will be tested by the reviewer: commands, dependencies, steps, etc.
-->

## Related issues

<!-- Recommended
Link related issues below. Insert the issue link or reference after the word "Closes" if merging this should automatically close it.

- Closes #123
- Relates #123
- Requires #123
- Superseds #123
-->
-

## Questions to ask yourself

- How are we going to support this in production?
- How are we going to measure its adoption?
- How are we going to debug this?
- What are the metrics I should take care of?
- ...

<!-- CI Cheatsheet
Trigger comments:
/test             (Or `buildkite test this|it`) Triggers unit test pipeline
/test extended    (Or `buildkite test extended`) Triggers integration test pipeline

PR labels:
skip-ci           Skips unit and integration tests
skip-it           Skips integration tests
-->


Linked issue #123: [Elastic Agent] Add ability to debug a composable provider
## Overview

To help support dynamic inputs elastic/beats#19225 Elastic Agent needs to add the ability to debug the providers using for variable substitution. This issue is to track the debugging effort, for information about variable substitution review elastic/beats#20781

## Debugging

This obviously adds a lot of confusion to what the resulting configuration that Elastic Agent will be running with. To ensure that the feature is deployed correctly and that providers are working as expected debugging needs to be a top priority in the implementation.

### Debugging the running daemon

With the new ability to communicate with the running daemon the `inspect` command should be changed to talk to the running daemon and return the current configuration that is being used in memory. This will ensure that with running providers like Docker and Kubernetes it is easy to inspect what the resulting configuration is.

The current `inspect` and `output` commands can be combined and moved under the `debug` subcommand. (**Note**: This is not connecting to the currently running Elastic Agent)

```
$ ./elastic-agent debug config
```

Possible to watch the configuration as changes come in with `--watch`.

```
$ ./elastic-agent debug config --watch
```

### Debugging a single provider

A new `debug` command should be implemented that runs a single provider and output what it's currently providing back to the Elastic Agent. (**Note**: This is not connecting to the currently running Elastic Agent)

Example outputting docker provider inventory key/value mappings:

```
$ ./elastic-agent debug provider docker 
{"id": "1",  "mapping": {"id": "1", "paths": {"log": "/var/log/containers/1.log"}}, "processors": {"add_fields": {"container.name": "my-container"}},}
{"id": "2", "mapping": {"id": "2", "paths": {"log": "/var/log/containers/2.log"}}, "processors": {"add_fields": {"container.name": "other-container"}},}
{"id": "2", "mapping": nil}
```

Example rendering configurations with changes:

```
$ ./elastic-agent debug provider docker -c testing-conf.yml
# {"id": "1",  "mapping": {"id": "1", "paths": {"log": "/var/log/containers/1.log"}}, "processors": {"add_fields": {"container.name": "my-container"}}}
inputs:
  - type: logfile
    path: /var/log/containers/1.log
    processors:
      - add_fields:
          container.name: my-container
# {"id": "2", "mapping": {"id": "2", "paths": {"log": "/var/log/containers/2.log"}}, "processors": {"add_fields": {"container.name": "other-container"}}}
inputs:
  - type: logfile
    path: /var/log/containers/1.log
    processors:
      - add_fields:
          container.name: my-container
  - type: logfile
    path: /var/log/containers/2.log
    processors:
      - add_fields:
          container.name: other-container
# {"id": "2", "mapping": nil}
inputs:
  - type: logfile
    path: /var/log/containers/1.log
Linked issue comments: user: login: elasticmachine
created_at: 2020-09-17T17:47:38Z
body: Pinging @elastic/ingest-management (Team:Ingest Management)
author_association: CONTRIBUTOR
user: login: ruflin
created_at: 2020-09-18T07:16:53Z
body: ++ on the proposal. I would skip the `--watch` part at first probably. I really like that the state of a provider can be read out and also the current running config.
author_association: CONTRIBUTOR
user: login: elasticmachine
created_at: 2021-10-29T10:25:47Z
body: Pinging @elastic/elastic-agent-control-plane  (Team:Elastic-Agent-Control-Plane)
author_association: CONTRIBUTOR
user: login: jlind23
created_at: 2021-10-29T15:14:29Z
body: @michel-laterman was it part of your diagnosis job? If yes, can we close this?`
author_association: CONTRIBUTOR
user: login: michel-laterman
created_at: 2021-10-29T16:16:33Z
body: The scope of the outlined issue is more then what is currently included with `diagnostics`, let's keep this open as I think we should add this to the command (eventually)
author_association: CONTRIBUTOR
user: login: michel-laterman
created_at: 2022-03-21T19:25:23Z
body: I want to add a little more details here as it relates to diagnostics.

Currently `diagnostics` will use the exact same output of `inspect` (and `inspect output`) when gathering config from the agent and running processes.
`diagnostics` also gathers its own copy of the local (`elastic-agent.yml`/`fleet.yml`) config files (I think with variable substitution?).

The `diagnostics` command should provide the process config/policies/etc from the running agent.

However, I don't think we should name the command that does not contact from to the running agent debug as we tend to use diagnostics and debug interchangeably; maybe add it as a diagnostic subcommand, or add a flag to `inspect`?
author_association: CONTRIBUTOR

Diff:
diff --git a/internal/pkg/agent/vault/aesgcm/aesgcm.go b/internal/pkg/agent/vault/aesgcm/aesgcm.go
index 3a5e21c4820..e26790ce85a 100644
--- a/internal/pkg/agent/vault/aesgcm/aesgcm.go
+++ b/internal/pkg/agent/vault/aesgcm/aesgcm.go
@@ -5,11 +5,8 @@
 package aesgcm
 
 import (
-	"crypto/aes"
-	"crypto/cipher"
 	"crypto/rand"
 	"encoding/hex"
-	"syscall"
 )
 
 // AESKeyType indicates the AES key length.
@@ -55,29 +52,6 @@ func NewKeyHexString(kt AESKeyType) (string, error) {
 	return hex.EncodeToString(key), nil
 }
 
-// Encrypt encrypts the data with AES-GCM
-func Encrypt(key, data []byte) ([]byte, error) {
-	block, err := aes.NewCipher(key)
-	if err != nil {
-		return nil, err
-	}
-
-	aesGCM, err := cipher.NewGCM(block)
-	if err != nil {
-		return nil, err
-	}
-
-	nonce := make([]byte, aesGCM.NonceSize())
-	if _, err = rand.Read(nonce); err != nil {
-		return nil, err
-	}
-
-	// The first parameter is nonce in order to get the ciphertext as concatenation of nonce and encrypted data
-	ciphertext := aesGCM.Seal(nonce, nonce, data, nil)
-
-	return ciphertext, nil
-}
-
 // EncryptHex encrypts with hex string key, producing hex encoded result
 func EncryptHex(key string, data []byte) (string, error) {
 	bkey, err := hex.DecodeString(key)
@@ -91,27 +65,6 @@ func EncryptHex(key string, data []byte) (string, error) {
 	return hex.EncodeToString(enc), nil
 }
 
-// Decrypt decrypts the data with AES-GCM
-func Decrypt(key, data []byte) ([]byte, error) {
-	block, err := aes.NewCipher(key)
-	if err != nil {
-		return nil, err
-	}
-
-	aesGCM, err := cipher.NewGCM(block)
-	if err != nil {
-		return nil, err
-	}
-
-	nonceSize := aesGCM.NonceSize()
-	if len(data) < nonceSize {
-		return nil, syscall.EINVAL
-	}
-	nonce, ciphertext := data[:nonceSize], data[nonceSize:]
-
-	return aesGCM.Open(nil, nonce, ciphertext, nil)
-}
-
 // DecryptHex decrypts with hex string key and data
 func DecryptHex(key string, data string) ([]byte, error) {
 	bkey, err := hex.DecodeString(key)
diff --git a/internal/pkg/agent/vault/aesgcm/aesgcm_fips.go b/internal/pkg/agent/vault/aesgcm/aesgcm_fips.go
new file mode 100644
index 00000000000..73700eba415
--- /dev/null
+++ b/internal/pkg/agent/vault/aesgcm/aesgcm_fips.go
@@ -0,0 +1,41 @@
+// 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 aesgcm
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+)
+
+// Encrypt encrypts the data with AES-GCM with random nonce
+func Encrypt(key, data []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+
+	aesGCM, err := cipher.NewGCMWithRandomNonce(block)
+	if err != nil {
+		return nil, err
+	}
+	ciphertext := aesGCM.Seal(nil, nil, data, nil)
+	return ciphertext, nil
+}
+
+// Decrypt decrypts the data with AES-GCM with random nonce
+func Decrypt(key, data []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+
+	aesGCM, err := cipher.NewGCMWithRandomNonce(block)
+	if err != nil {
+		return nil, err
+	}
+	return aesGCM.Open(nil, nil, data, nil)
+}
diff --git a/internal/pkg/agent/vault/aesgcm/aesgcm_nofips.go b/internal/pkg/agent/vault/aesgcm/aesgcm_nofips.go
new file mode 100644
index 00000000000..9af6969385f
--- /dev/null
+++ b/internal/pkg/agent/vault/aesgcm/aesgcm_nofips.go
@@ -0,0 +1,56 @@
+// 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 aesgcm
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/rand"
+	"syscall"
+)
+
+// Encrypt encrypts the data with AES-GCM
+func Encrypt(key, data []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+
+	aesGCM, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	nonce := make([]byte, aesGCM.NonceSize())
+	if _, err = rand.Read(nonce); err != nil {
+		return nil, err
+	}
+
+	// The first parameter is nonce in order to get the ciphertext as concatenation of nonce and encrypted data
+	ciphertext := aesGCM.Seal(nonce, nonce, data, nil)
+	return ciphertext, nil
+}
+
+// Decrypt decrypts the data with AES-GCM
+func Decrypt(key, data []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+
+	aesGCM, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	nonceSize := aesGCM.NonceSize()
+	if len(data) < nonceSize {
+		return nil, syscall.EINVAL
+	}
+	nonce, ciphertext := data[:nonceSize], data[nonceSize:]
+	return aesGCM.Open(nil, nonce, ciphertext, nil)
+}
diff --git a/internal/pkg/crypto/io.go b/internal/pkg/crypto/io.go
index a65d2de1334..b559e517c9a 100644
--- a/internal/pkg/crypto/io.go
+++ b/internal/pkg/crypto/io.go
@@ -11,36 +11,11 @@ import (
 	"crypto/pbkdf2"
 	"crypto/rand"
 	"crypto/sha512"
-	"encoding/binary"
 	"errors"
 	"fmt"
 	"io"
 )
 
-// Option is the default options used to generate the encrypt and decrypt writer.
-// NOTE: the defined options need to be same for both the Reader and the writer.
-type Option struct {
-	Generator       bytesGen
-	IterationsCount int
-	KeyLength       int
-	SaltLength      int
-	IVLength        int
-
-	// BlockSize must be a factor of aes.BlockSize
-	BlockSize int
-}
-
-// DefaultOptions is the default options to use when creating the writer, changing might decrease
-// the efficacity of the encryption.
-var DefaultOptions = &Option{
-	IterationsCount: 10000,
-	KeyLength:       32,
-	SaltLength:      64,
-	IVLength:        12,
-	Generator:       randomBytes,
-	BlockSize:       bytes.MinRead,
-}
-
 // versionMagicHeader is the format version that will be added at the beginning of the header and
 // can be used to change how the decryption work in future version.
 var versionMagicHeader = []byte("v2")
@@ -122,7 +97,7 @@ func (w *Writer) Write(b []byte) (int, error) {
 			return 0, w.err
 		}
 
-		aesgcm, err := cipher.NewGCM(block)
+		aesgcm, err := getCipherAEAD(block)
 		if err != nil {
 			w.err = fmt.Errorf("could not create the GCM to encrypt: %w", err)
 			return 0, w.err
@@ -160,33 +135,6 @@ func (w *Writer) Write(b []byte) (int, error) {
 	return len(b), nil
 }
 
-func (w *Writer) writeBlock(b []byte) error {
-	// randomly generate the salt and the initialization vector, this information will be saved
-	// on disk in the file as part of the header
-	iv, err := w.generator(w.option.IVLength)
-	if err != nil {
-		w.err = fmt.Errorf("fail to generate random IV: %w", err)
-		return w.err
-	}
-
-	//nolint:errcheck // Ignore the error at this point.
-	w.writer.Write(iv)
-
-	encodedBytes := w.gcm.Seal(nil, iv, b, nil)
-
-	l := make([]byte, 4)
-	binary.LittleEndian.PutUint32(l, uint32(len(encodedBytes))) //nolint:gosec // ignoring unsafe type conversion
-	//nolint:errcheck // Ignore the error at this point.
-	w.writer.Write(l)
-
-	_, err = w.writer.Write(encodedBytes)
-	if err != nil {
-		return fmt.Errorf("fail to encode data: %w", err)
-	}
-
-	return nil
-}
-
 // Reader implements the io.Reader interface and allow to decrypt bytes from the Writer. The reader
 // will lazy read the header from the wrapper reader to initialize everything required to decrypt
 // the data.
@@ -262,7 +210,7 @@ func (r *Reader) Read(b []byte) (int, error) {
 			return 0, r.err
 		}
 
-		aesgcm, err := cipher.NewGCM(block)
+		aesgcm, err := getCipherAEAD(block)
 		if err != nil {
 			r.err = fmt.Errorf("could not create the GCM to decrypt the data: %w", err)
 			return 0, r.err
@@ -300,43 +248,6 @@ func (r *Reader) readTo(b []byte) (int, error) {
 	return n, r.err
 }
 
-func (r *Reader) consumeBlock() error {
-	// Retrieve block information:
-	// - Initialization vector
-	// - Length of the block
-	iv, l, err := r.readBlockInfo()
-	if err != nil {
-		return err
-	}
-
-	encodedBytes := make([]byte, l)
-	_, err = io.ReadAtLeast(r.reader, encodedBytes, l)
-	if err != nil {
-		r.err = fmt.Errorf("fail read the block of %d bytes: %w", l, err)
-	}
-
-	decodedBytes, err := r.gcm.Open(nil, iv, encodedBytes, nil)
-	if err != nil {
-		return fmt.Errorf("fail to decode bytes: %w", err)
-	}
-	r.buf = append(r.buf[:], decodedBytes...)
-
-	return nil
-}
-
-func (r *Reader) readBlockInfo() ([]byte, int, error) {
-	buf := make([]byte, r.option.IVLength+4)
-	_, err := io.ReadAtLeast(r.reader, buf, len(buf))
-	if err != nil {
-		return nil, 0, err
-	}
-
-	iv := buf[0:r.option.IVLength]
-	l := binary.LittleEndian.Uint32(buf[r.option.IVLength:])
-
-	return iv, int(l), nil
-}
-
 // Close will propagate the Close call to the wrapped reader.
 func (r *Reader) Close() error {
 	a, ok := r.reader.(io.ReadCloser)
diff --git a/internal/pkg/crypto/io_fips.go b/internal/pkg/crypto/io_fips.go
new file mode 100644
index 00000000000..ea9588d835f
--- /dev/null
+++ b/internal/pkg/crypto/io_fips.go
@@ -0,0 +1,104 @@
+// 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 crypto
+
+import (
+	"bytes"
+	"crypto/cipher"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+)
+
+// Option is the default options used to generate the encrypt and decrypt writer.
+// NOTE: the defined options need to be same for both the Reader and the writer.
+type Option struct {
+	Generator       bytesGen
+	IterationsCount int
+	KeyLength       int
+	SaltLength      int
+
+	// BlockSize must be a factor of aes.BlockSize
+	BlockSize int
+}
+
+// DefaultOptions is the default options to use when creating the writer, changing might decrease
+// the efficacity of the encryption.
+var DefaultOptions = &Option{
+	IterationsCount: 10000,
+	KeyLength:       32,
+	SaltLength:      64,
+	Generator:       randomBytes,
+	BlockSize:       bytes.MinRead,
+}
+
+// Validate the options for encoding and decoding values.
+func (o *Option) Validate() error {
+	if o.SaltLength < 16 {
+		return errors.New("SaltLength must be at least 128 bits (16 bytes)")
+	}
+
+	if o.IterationsCount < 1000 {
+		return errors.New("IterationsCount must be at least 1000")
+	}
+
+	if o.KeyLength < 14 {
+		return errors.New("KeyLength must be at least 112 bits (14 bytes)")
+	}
+
+	return nil
+}
+
+func getCipherAEAD(block cipher.Block) (cipher.AEAD, error) {
+	return cipher.NewGCMWithRandomNonce(block)
+}
+
+func (w *Writer) writeBlock(b []byte) error {
+	encodedBytes := w.gcm.Seal(nil, nil, b, nil)
+
+	l := make([]byte, 4)
+	binary.LittleEndian.PutUint32(l, uint32(len(encodedBytes)))
+	_, err := w.writer.Write(l)
+	if err != nil {
+		return fmt.Errorf("fail to write len of encoded data: %w", err)
+	}
+	_, err = w.writer.Write(encodedBytes)
+	if err != nil {
+		return fmt.Errorf("fail to encode data: %w", err)
+	}
+	return nil
+}
+
+func (r *Reader) consumeBlock() error {
+	// Retrieve block information:
+	// - Length of the block
+	l, err := r.readBlockInfo()
+	if err != nil {
+		return err
+	}
+	encodedBytes := make([]byte, l)
+	_, err = io.ReadAtLeast(r.reader, encodedBytes, l)
+	if err != nil {
+		r.err = fmt.Errorf("fail read the block of %d bytes: %w", l, err)
+	}
+	decodedBytes, err := r.gcm.Open(nil, nil, encodedBytes, nil)
+	if err != nil {
+		return fmt.Errorf("fail to decode bytes: %w", err)
+	}
+	r.buf = append(r.buf[:], decodedBytes...)
+	return nil
+}
+
+func (r *Reader) readBlockInfo() (int, error) {
+	buf := make([]byte, 4)
+	if _, err := io.ReadAtLeast(r.reader, buf, len(buf)); err != nil {
+		return 0, err
+	}
+	l := binary.LittleEndian.Uint32(buf)
+	return int(l), nil
+}
diff --git a/internal/pkg/crypto/io_opts_fips_test.go b/internal/pkg/crypto/io_fips_test.go
similarity index 83%
rename from internal/pkg/crypto/io_opts_fips_test.go
rename to internal/pkg/crypto/io_fips_test.go
index e98f0a39a79..ab262fc9853 100644
--- a/internal/pkg/crypto/io_opts_fips_test.go
+++ b/internal/pkg/crypto/io_fips_test.go
@@ -22,17 +22,8 @@ func Test_Option_ValidateFIPS(t *testing.T) {
 		name   string
 		option *Option
 	}{{
-		name: "IVLength is low",
-		option: &Option{
-			IVLength:        10,
-			SaltLength:      20,
-			IterationsCount: 10000,
-			KeyLength:       20,
-		},
-	}, {
 		name: "SaltLength is low",
 		option: &Option{
-			IVLength:        20,
 			SaltLength:      10,
 			IterationsCount: 10000,
 			KeyLength:       20,
@@ -40,7 +31,6 @@ func Test_Option_ValidateFIPS(t *testing.T) {
 	}, {
 		name: "IterationsCount is low",
 		option: &Option{
-			IVLength:        20,
 			SaltLength:      20,
 			IterationsCount: 100,
 			KeyLength:       20,
@@ -48,7 +38,6 @@ func Test_Option_ValidateFIPS(t *testing.T) {
 	}, {
 		name: "KeyLength is low",
 		option: &Option{
-			IVLength:        20,
 			SaltLength:      20,
 			IterationsCount: 10000,
 			KeyLength:       10,
diff --git a/internal/pkg/crypto/io_nofips.go b/internal/pkg/crypto/io_nofips.go
new file mode 100644
index 00000000000..128aa2f92f5
--- /dev/null
+++ b/internal/pkg/crypto/io_nofips.go
@@ -0,0 +1,129 @@
+// 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 crypto
+
+import (
+	"bytes"
+	"crypto/cipher"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+)
+
+// Option is the default options used to generate the encrypt and decrypt writer.
+// NOTE: the defined options need to be same for both the Reader and the writer.
+type Option struct {
+	Generator       bytesGen
+	IterationsCount int
+	KeyLength       int
+	SaltLength      int
+	IVLength        int
+
+	// BlockSize must be a factor of aes.BlockSize
+	BlockSize int
+}
+
+// DefaultOptions is the default options to use when creating the writer, changing might decrease
+// the efficacity of the encryption.
+var DefaultOptions = &Option{
+	IterationsCount: 10000,
+	KeyLength:       32,
+	SaltLength:      64,
+	IVLength:        12,
+	Generator:       randomBytes,
+	BlockSize:       bytes.MinRead,
+}
+
+// Validate the options for encoding and decoding values.
+func (o *Option) Validate() error {
+	if o.IVLength == 0 {
+		return errors.New("IVLength must be superior to 0")
+	}
+
+	if o.SaltLength == 0 {
+		return errors.New("SaltLength must be superior to 0")
+	}
+
+	if o.IterationsCount == 0 {
+		return errors.New("IterationsCount must be superior to 0")
+	}
+
+	if o.KeyLength == 0 {
+		return errors.New("KeyLength must be superior to 0")
+	}
+
+	return nil
+}
+
+func getCipherAEAD(block cipher.Block) (cipher.AEAD, error) {
+	return cipher.NewGCM(block)
+}
+
+func (w *Writer) writeBlock(b []byte) error {
+	// randomly generate the salt and the initialization vector, this information will be saved
+	// on disk in the file as part of the header
+	iv, err := w.generator(w.option.IVLength)
+	if err != nil {
+		w.err = fmt.Errorf("fail to generate random IV: %w", err)
+		return w.err
+	}
+
+	//nolint:errcheck // Ignore the error at this point.
+	w.writer.Write(iv)
+
+	encodedBytes := w.gcm.Seal(nil, iv, b, nil)
+
+	l := make([]byte, 4)
+	binary.LittleEndian.PutUint32(l, uint32(len(encodedBytes))) //nolint:gosec // ignoring unsafe type conversion
+	//nolint:errcheck // Ignore the error at this point.
+	w.writer.Write(l)
+
+	_, err = w.writer.Write(encodedBytes)
+	if err != nil {
+		return fmt.Errorf("fail to encode data: %w", err)
+	}
+
+	return nil
+}
+
+func (r *Reader) consumeBlock() error {
+	// Retrieve block information:
+	// - Initialization vector
+	// - Length of the block
+	iv, l, err := r.readBlockInfo()
+	if err != nil {
+		return err
+	}
+
+	encodedBytes := make([]byte, l)
+	_, err = io.ReadAtLeast(r.reader, encodedBytes, l)
+	if err != nil {
+		r.err = fmt.Errorf("fail read the block of %d bytes: %w", l, err)
+	}
+
+	decodedBytes, err := r.gcm.Open(nil, iv, encodedBytes, nil)
+	if err != nil {
+		return fmt.Errorf("fail to decode bytes: %w", err)
+	}
+	r.buf = append(r.buf[:], decodedBytes...)
+
+	return nil
+}
+
+func (r *Reader) readBlockInfo() ([]byte, int, error) {
+	buf := make([]byte, r.option.IVLength+4)
+	_, err := io.ReadAtLeast(r.reader, buf, len(buf))
+	if err != nil {
+		return nil, 0, err
+	}
+
+	iv := buf[0:r.option.IVLength]
+	l := binary.LittleEndian.Uint32(buf[r.option.IVLength:])
+
+	return iv, int(l), nil
+}
diff --git a/internal/pkg/crypto/io_opts_nofips_test.go b/internal/pkg/crypto/io_nofips_test.go
similarity index 100%
rename from internal/pkg/crypto/io_opts_nofips_test.go
rename to internal/pkg/crypto/io_nofips_test.go
diff --git a/internal/pkg/crypto/io_opts_fips.go b/internal/pkg/crypto/io_opts_fips.go
deleted file mode 100644
index 0a20af37f8b..00000000000
--- a/internal/pkg/crypto/io_opts_fips.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// 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 crypto
-
-import (
-	"errors"
-)
-
-// Validate the options for encoding and decoding values.
-func (o *Option) Validate() error {
-	if o.IVLength < 12 {
-		return errors.New("IVLength must be at least 96 bits (12 bytes)")
-	}
-
-	if o.SaltLength < 16 {
-		return errors.New("SaltLength must be at least 128 bits (16 bytes)")
-	}
-
-	if o.IterationsCount < 1000 {
-		return errors.New("IterationsCount must be at least 1000")
-	}
-
-	if o.KeyLength < 14 {
-		return errors.New("KeyLength must be at least 112 bits (14 bytes)")
-	}
-
-	return nil
-}
diff --git a/internal/pkg/crypto/io_opts_nofips.go b/internal/pkg/crypto/io_opts_nofips.go
deleted file mode 100644
index dbf71cf57b4..00000000000
--- a/internal/pkg/crypto/io_opts_nofips.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// 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 crypto
-
-import (
-	"errors"
-)
-
-// Validate the options for encoding and decoding values.
-func (o *Option) Validate() error {
-	if o.IVLength == 0 {
-		return errors.New("IVLength must be superior to 0")
-	}
-
-	if o.SaltLength == 0 {
-		return errors.New("SaltLength must be superior to 0")
-	}
-
-	if o.IterationsCount == 0 {
-		return errors.New("IterationsCount must be superior to 0")
-	}
-
-	if o.KeyLength == 0 {
-		return errors.New("KeyLength must be superior to 0")
-	}
-
-	return nil
-}


Reviews:
user: login: blakerouse
body: This looks good to me, but also want to get @michel-laterman review on it. Also be sure to set the proper backport labels on this.
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T13:54:07Z
user: login: michel-laterman
body: lgtm, please just clean up the extra comment before merging
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T15:27:58Z
user: login: michel-laterman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T14:43:41Z
user: login: simitt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T18:19:46Z
user: login: michel-laterman
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T18:44:34Z

Review comments:
diff_hunk: @@ -0,0 +1,47 @@
+// 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 aesgcm
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+)
+
+func getCipherAEAD(block cipher.Block) (cipher.AEAD, error) {
+	return cipher.NewGCMWithRandomNonce(block)
+}
+
+// Encrypt encrypts the data with AES-GCM
+func Encrypt(key, data []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+
+	aesGCM, err := cipher.NewGCMWithRandomNonce(block)
+	if err != nil {
+		return nil, err
+	}
+
+	// The first parameter is nonce in order to get the ciphertext as concatenation of nonce and encrypted data
path: internal/pkg/agent/vault/aesgcm/aesgcm_fips.go
user: login: michel-laterman
body: We should remove this comment from the fips code
created_at: 2025-03-24T15:18:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,45 @@
+// 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 aesgcm
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+)
+
+func getCipherAEAD(block cipher.Block) (cipher.AEAD, error) {
+	return cipher.NewGCMWithRandomNonce(block)
+}
path: internal/pkg/agent/vault/aesgcm/aesgcm_fips.go
user: login: michel-laterman
body: Is this used?
created_at: 2025-03-25T14:43:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,45 @@
+// 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 aesgcm
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+)
+
+func getCipherAEAD(block cipher.Block) (cipher.AEAD, error) {
+	return cipher.NewGCMWithRandomNonce(block)
+}
path: internal/pkg/agent/vault/aesgcm/aesgcm_fips.go
user: login: simitt
body: no, left over - good catch
created_at: 2025-03-25T18:19:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: mergify[bot]
created_at: 2025-03-19T17:24:27Z
body: This pull request does not have a backport label. Could you fix it @simitt? 🙏
To fixup this pull request, you need to add the backport labels for the needed
branches, such as:
* `backport-./d./d` is the label that automatically backports to the `8./d` branch. `/d` is the digit
* `backport-active-all` is the label that automatically backports to all active branches.
* `backport-active-8` is the label that automatically backports to all active minor branches for the 8 major.
* `backport-active-9` is the label that automatically backports to all active minor branches for the 9 major.
author_association: CONTRIBUTOR
user: login: elasticmachine
created_at: 2025-03-21T14:15:36Z
body: Pinging @elastic/elastic-agent-control-plane (Team:Elastic-Agent-Control-Plane)
author_association: CONTRIBUTOR
user: login: elastic-sonarqube[bot]
created_at: 2025-03-25T19:17:50Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonar.elastic.dev/dashboard?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&pullRequest=7474) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonar.elastic.dev/project/issues?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&pullRequest=7474&resolved=false) [0 New issues](https://sonar.elastic.dev/project/issues?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&pullRequest=7474&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://sonar.elastic.dev/project/issues?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&fixedInPullRequest=7474) [0 Fixed issues](https://sonar.elastic.dev/project/issues?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&fixedInPullRequest=7474)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://sonar.elastic.dev/project/issues?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&pullRequest=7474&issueStatuses=ACCEPTED) [0 Accepted issues](https://sonar.elastic.dev/project/issues?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&pullRequest=7474&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonar.elastic.dev/security_hotspots?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&pullRequest=7474) [0 Security Hotspots](https://sonar.elastic.dev/security_hotspots?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&pullRequest=7474)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonar.elastic.dev/component_measures?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&pullRequest=7474&metric=new_coverage&view=list) [55.2% Coverage on New Code](https://sonar.elastic.dev/component_measures?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&pullRequest=7474&metric=new_coverage&view=list)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://sonar.elastic.dev/component_measures?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&pullRequest=7474&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://sonar.elastic.dev/component_measures?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&pullRequest=7474&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://sonar.elastic.dev/dashboard?id=elastic_elastic-agent_AYluowg0xMq8P7b4moiZ&pullRequest=7474)


author_association: NONE
user: login: elasticmachine
created_at: 2025-03-25T20:01:16Z
body: ## :yellow_heart: Build succeeded, but was flaky
* [Buildkite Build](https://buildkite.com/elastic/elastic-agent/builds/19159)
* Commit: b425ca01c24d5d9d8214d23aff4db28706449b3c

### Failed CI Steps
* [x86_64:sudo: fleet-privileged](https://buildkite.com/elastic/elastic-agent/builds/19159#0195cea8-3dc5-429e-962c-decb6cec316d)

### History
* :green_heart: [Build #19139](https://buildkite.com/elastic/elastic-agent/builds/19139) succeeded c591feeeffc2cf2a461163151aadc097c02d7d48
* :broken_heart: [Build #19130](https://buildkite.com/elastic/elastic-agent/builds/19130) failed bf0490909ead17f1e1717a980995409ea144fe1d
* :yellow_heart: [Build #19051](https://buildkite.com/elastic/elastic-agent/builds/19051) was flaky 4cf124a1e8bcd8a56b2e3c33dc6d39ac4da79d27
* :broken_heart: [Build #19032](https://buildkite.com/elastic/elastic-agent/builds/19032) failed 4a1296dd95d3a31b891568e089bbcc69689901c2
* :broken_heart: [Build #18988](https://buildkite.com/elastic/elastic-agent/builds/18988) failed 11a7dd758dbfe7067b21f634aed18f8c396b9270
* :broken_heart: [Build #18980](https://buildkite.com/elastic/elastic-agent/builds/18980) failed b1172fbd5714be02051a26bc7f71c35217cbed3b

cc @simitt

<!--buildkite-pr-comment-elastic-agent
{"builds":[{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/elastic-agent/builds/19159","number":19159,"commit":"b425ca01c24d5d9d8214d23aff4db28706449b3c"},{"buildStatus":{"state":"passed","success":true,"hasRetries":false,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/elastic-agent/builds/19139","number":19139,"commit":"c591feeeffc2cf2a461163151aadc097c02d7d48"},{"buildStatus":{"state":"failed","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/elastic-agent/builds/19130","number":19130,"commit":"bf0490909ead17f1e1717a980995409ea144fe1d"},{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/elastic-agent/builds/19051","number":19051,"commit":"4cf124a1e8bcd8a56b2e3c33dc6d39ac4da79d27"},{"buildStatus":{"state":"failed","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/elastic-agent/builds/19032","number":19032,"commit":"4a1296dd95d3a31b891568e089bbcc69689901c2"},{"buildStatus":{"state":"failing","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/elastic-agent/builds/18988","number":18988,"commit":"11a7dd758dbfe7067b21f634aed18f8c396b9270"}],"number":19159}
buildkite-pr-comment-->
author_association: CONTRIBUTOR