Repository: metal3-io/baremetal-operator (go)

Issue:
E2E: Implement SSH Key-Based Authentication for Live-ISO Provisioning
**User Story**

As a developer, I would like to enhance the virtuamedia/live-ISO provisioning process by implementing SSH key-based authentication in our e2e tests, to improve security and configuration flexibility.

**Detailed Description**

In our current setup, we test virtuamedia/live-ISO provisioning by SSH-ing into a node that booted from a live-ISO, using a password for authentication. While functional, this method lacks the robustness and security that SSH key-based authentication offers.

The goal is to make the SSH authentication method configurable, allowing the choice between password and SSH key-based access. This enhancement will initially be applied to the live-ISO provisioning test. Furthermore, part of this enhancement includes preparing the groundwork for future capabilities where users can use their own images and SSH keys, enhancing customization and security.

Post-implementation, we should evaluate extending this feature to other provisioning tests, ensuring a uniform and secure approach across the board.

**Anything else you would like to add:**

It's also worth discussing the potential of backporting this feature to other modules, like CAPM3, for consistency and security enhancement across our projects.

/kind feature


Pull request description:
🌱 E2e Add SSH Key-Based Authentication
Fixes #1473


Patch:
diff --git a/README.md b/README.md
index 1a277a56..cd860544 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,5 @@
 # Bare Metal Operator
 
-[![CLOMonitor](https://img.shields.io/endpoint?url=https://clomonitor.io/api/projects/cncf/metal3-io/badge)](https://clomonitor.io/projects/cncf/metal3-io)
 [![Ubuntu daily main build status](https://jenkins.nordix.org/buildStatus/icon?job=metal3_daily_main_integration_test_ubuntu&subject=Ubuntu%20daily%20main)](https://jenkins.nordix.org/view/Metal3/job/metal3_daily_main_integration_test_ubuntu/)
 [![CentOS daily main build status](https://jenkins.nordix.org/buildStatus/icon?job=metal3_daily_main_integration_test_centos&subject=CentOS%20daily%20main)](https://jenkins.nordix.org/view/Metal3/job/metal3_daily_main_integration_test_centos/)
 
diff --git a/apis/go.mod b/apis/go.mod
index 89383ba6..f90eaec1 100644
--- a/apis/go.mod
+++ b/apis/go.mod
@@ -3,7 +3,7 @@ module github.com/metal3-io/baremetal-operator/apis
 go 1.20
 
 require (
-	github.com/google/uuid v1.5.0
+	github.com/google/uuid v1.4.0
 	github.com/metal3-io/baremetal-operator/pkg/hardwareutils v0.4.0
 	github.com/stretchr/testify v1.8.4
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
diff --git a/apis/go.sum b/apis/go.sum
index 8eb7b555..18c45998 100644
--- a/apis/go.sum
+++ b/apis/go.sum
@@ -58,8 +58,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
 github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
-github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
-github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
+github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
diff --git a/apis/metal3.io/v1alpha1/baremetalhost_types.go b/apis/metal3.io/v1alpha1/baremetalhost_types.go
index af629b64..15189eac 100644
--- a/apis/metal3.io/v1alpha1/baremetalhost_types.go
+++ b/apis/metal3.io/v1alpha1/baremetalhost_types.go
@@ -450,7 +450,7 @@ const (
 )
 
 // ChecksumType holds the algorithm name for the checksum
-// +kubebuilder:validation:Enum=md5;sha256;sha512;auto
+// +kubebuilder:validation:Enum=md5;sha256;sha512
 type ChecksumType string
 
 const (
@@ -462,9 +462,6 @@ const (
 
 	// SHA512 checksum type
 	SHA512 ChecksumType = "sha512"
-
-	// Automatically detect
-	AutoChecksum ChecksumType = "auto"
 )
 
 // Image holds the details of an image either to provisioned or that
@@ -476,9 +473,8 @@ type Image struct {
 	// Checksum is the checksum for the image.
 	Checksum string `json:"checksum,omitempty"`
 
-	// ChecksumType is the checksum algorithm for the image, e.g md5, sha256 or sha512.
-	// The special value "auto" can be used to detect the algorithm from the checksum.
-	// If missing, MD5 is used. If in doubt, use "auto".
+	// ChecksumType is the checksum algorithm for the image.
+	// e.g md5, sha256, sha512
 	ChecksumType ChecksumType `json:"checksumType,omitempty"`
 
 	// DiskFormat contains the format of the image (raw, qcow2, ...).
@@ -1100,8 +1096,6 @@ func (image *Image) GetChecksum() (checksum, checksumType string, ok bool) {
 		checksumType = string(MD5)
 	case MD5, SHA256, SHA512:
 		checksumType = string(image.ChecksumType)
-	case AutoChecksum:
-		// No type, let Ironic detect
 	default:
 		return
 	}
diff --git a/cmd/make-bm-worker/main.go b/cmd/make-bm-worker/main.go
index 5eac99d8..5394d89b 100644
--- a/cmd/make-bm-worker/main.go
+++ b/cmd/make-bm-worker/main.go
@@ -62,9 +62,9 @@ func main() {
 	}
 
 	switch *imageChecksumType {
-	case "", "md5", "sha256", "sha512", "auto":
+	case "", "md5", "sha256", "sha512":
 	default:
-		fmt.Fprintf(os.Stderr, "Invalid image checksum type %q, use \"md5\", \"sha256\", \"sha512\" or \"auto\"\n", *imageChecksumType)
+		fmt.Fprintf(os.Stderr, "Invalid image checksum type %q, use \"md5\", \"sha256\" or \"sha512\"\n", *imageChecksumType)
 		os.Exit(1)
 	}
 
diff --git a/config/base/crds/bases/metal3.io_baremetalhosts.yaml b/config/base/crds/bases/metal3.io_baremetalhosts.yaml
index d2aa2121..77a57565 100644
--- a/config/base/crds/bases/metal3.io_baremetalhosts.yaml
+++ b/config/base/crds/bases/metal3.io_baremetalhosts.yaml
@@ -221,15 +221,12 @@ spec:
                     description: Checksum is the checksum for the image.
                     type: string
                   checksumType:
-                    description: ChecksumType is the checksum algorithm for the image,
-                      e.g md5, sha256 or sha512. The special value "auto" can be used
-                      to detect the algorithm from the checksum. If missing, MD5 is
-                      used. If in doubt, use "auto".
+                    description: ChecksumType is the checksum algorithm for the image.
+                      e.g md5, sha256, sha512
                     enum:
                     - md5
                     - sha256
                     - sha512
-                    - auto
                     type: string
                   format:
                     description: DiskFormat contains the format of the image (raw,
@@ -874,14 +871,11 @@ spec:
                         type: string
                       checksumType:
                         description: ChecksumType is the checksum algorithm for the
-                          image, e.g md5, sha256 or sha512. The special value "auto"
-                          can be used to detect the algorithm from the checksum. If
-                          missing, MD5 is used. If in doubt, use "auto".
+                          image. e.g md5, sha256, sha512
                         enum:
                         - md5
                         - sha256
                         - sha512
-                        - auto
                         type: string
                       format:
                         description: DiskFormat contains the format of the image (raw,
diff --git a/config/render/capm3.yaml b/config/render/capm3.yaml
index f013ce50..ba6d83f1 100644
--- a/config/render/capm3.yaml
+++ b/config/render/capm3.yaml
@@ -221,15 +221,12 @@ spec:
                     description: Checksum is the checksum for the image.
                     type: string
                   checksumType:
-                    description: ChecksumType is the checksum algorithm for the image,
-                      e.g md5, sha256 or sha512. The special value "auto" can be used
-                      to detect the algorithm from the checksum. If missing, MD5 is
-                      used. If in doubt, use "auto".
+                    description: ChecksumType is the checksum algorithm for the image.
+                      e.g md5, sha256, sha512
                     enum:
                     - md5
                     - sha256
                     - sha512
-                    - auto
                     type: string
                   format:
                     description: DiskFormat contains the format of the image (raw,
@@ -874,14 +871,11 @@ spec:
                         type: string
                       checksumType:
                         description: ChecksumType is the checksum algorithm for the
-                          image, e.g md5, sha256 or sha512. The special value "auto"
-                          can be used to detect the algorithm from the checksum. If
-                          missing, MD5 is used. If in doubt, use "auto".
+                          image. e.g md5, sha256, sha512
                         enum:
                         - md5
                         - sha256
                         - sha512
-                        - auto
                         type: string
                       format:
                         description: DiskFormat contains the format of the image (raw,
diff --git a/docs/api.md b/docs/api.md
index 1f0ed8f1..5e9432b3 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -91,8 +91,8 @@ The sub-fields are
 * *checksum* -- The actual checksum or a URL to a file containing
   the checksum for the image at *image.url*.
 * *checksumType* -- Checksum algorithms can be specified. Currently
-  only `md5`, `sha256`, `sha512` are recognized. Use `auto` to auto-detect
-  the algorithm from the value. If nothing is specified, `md5` is assumed.
+  only `md5`, `sha256`, `sha512` are recognized. If nothing is specified
+  `md5` is assumed.
 * *format* -- This is the disk format of the image. It can be one of `raw`,
   `qcow2`, `vdi`, `vmdk`, `live-iso` or be left unset.
   Setting it to raw enables raw image streaming in Ironic agent for that image.
diff --git a/go.mod b/go.mod
index d21e293e..75e08da6 100644
--- a/go.mod
+++ b/go.mod
@@ -43,7 +43,7 @@ require (
 	github.com/google/gnostic v0.5.7-v3refs // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/google/gofuzz v1.1.0 // indirect
-	github.com/google/uuid v1.5.0 // indirect
+	github.com/google/uuid v1.4.0 // indirect
 	github.com/imdario/mergo v0.3.12 // indirect
 	github.com/inconshreveable/mousetrap v1.0.1 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index b4ae355f..7a348be2 100644
--- a/go.sum
+++ b/go.sum
@@ -71,8 +71,8 @@ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
 github.com/google/safetext v0.0.0-20230106111101-7156a760e523 h1:i4NsbmB9pD5+Ggp5GZKyvYY6MkjvPE8CIMlkvXFF8gA=
 github.com/google/safetext v0.0.0-20230106111101-7156a760e523/go.mod h1:mJNEy0r5YPHC7ChQffpOszlGB4L1iqjXWpIEKcFpr9s=
-github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
-github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
+github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gophercloud/gophercloud v1.5.1-0.20231117122435-08456f7fe42e h1:acHzJUXVfS+GImmi+yyM2+AOiaO94Z4KaTTFVU6Ov9U=
 github.com/gophercloud/gophercloud v1.5.1-0.20231117122435-08456f7fe42e/go.mod h1:ecnaHbFxBunc2UeAONDQbkQacv6WP1syeZ02TgAB7kA=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index 45b4d1b2..88ed19e0 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -965,21 +965,26 @@ func (p *ironicProvisioner) setDirectDeployUpdateOptsForNode(ironicNode *nodes.N
 		return
 	}
 
+	// FIXME: For older versions of ironic that do not have
+	// https://review.opendev.org/#/c/711816/ failing to include the
+	// 'image_checksum' causes ironic to refuse to provision the
+	// image, even if the other hash value parameters are given. We
+	// only want to do that for MD5, however, because those versions
+	// of ironic only support MD5 checksums.
+	var legacyChecksum *string
+	if checksumType == string(metal3api.MD5) {
+		legacyChecksum = &checksum
+	}
+
 	optValues := optionsData{
 		// Remove any boot_iso field
-		"boot_iso":          nil,
-		"image_source":      imageData.URL,
-		"image_disk_format": imageData.DiskFormat,
-	}
+		"boot_iso": nil,
 
-	if checksumType == "" {
-		optValues["image_checksum"] = checksum
-		optValues["image_os_hash_algo"] = nil
-		optValues["image_os_hash_value"] = nil
-	} else {
-		optValues["image_checksum"] = nil
-		optValues["image_os_hash_algo"] = checksumType
-		optValues["image_os_hash_value"] = checksum
+		"image_source":        imageData.URL,
+		"image_os_hash_algo":  checksumType,
+		"image_os_hash_value": checksum,
+		"image_checksum":      legacyChecksum,
+		"image_disk_format":   imageData.DiskFormat,
 	}
 	updater.
 		SetInstanceInfoOpts(optValues, ironicNode)
@@ -1277,24 +1282,15 @@ func (p *ironicProvisioner) ironicHasSameImage(ironicNode *nodes.Node, image met
 			"provisionState", ironicNode.ProvisionState)
 	} else {
 		checksum, checksumType, _ := image.GetChecksum()
-		if checksumType == "" {
-			sameImage = (ironicNode.InstanceInfo["image_source"] == image.URL &&
-				ironicNode.InstanceInfo["image_checksum"] == checksum &&
-				ironicNode.InstanceInfo["image_os_hash_algo"] == nil &&
-				ironicNode.InstanceInfo["image_os_hash_value"] == nil)
-		} else {
-			sameImage = (ironicNode.InstanceInfo["image_source"] == image.URL &&
-				ironicNode.InstanceInfo["image_checksum"] == nil &&
-				ironicNode.InstanceInfo["image_os_hash_algo"] == checksumType &&
-				ironicNode.InstanceInfo["image_os_hash_value"] == checksum)
-		}
+		sameImage = (ironicNode.InstanceInfo["image_source"] == image.URL &&
+			ironicNode.InstanceInfo["image_os_hash_algo"] == checksumType &&
+			ironicNode.InstanceInfo["image_os_hash_value"] == checksum)
 		p.log.Info("checking image settings",
 			"source", ironicNode.InstanceInfo["image_source"],
-			"checksumType", checksumType,
-			"checksum", checksum,
+			"image_os_hash_algo", checksumType,
+			"image_os_has_value", checksum,
 			"same", sameImage,
-			"provisionState", ironicNode.ProvisionState,
-			"iinfo", ironicNode.InstanceInfo)
+			"provisionState", ironicNode.ProvisionState)
 	}
 	return sameImage
 }


Test patch:
diff --git a/apis/metal3.io/v1alpha1/baremetalhost_types_test.go b/apis/metal3.io/v1alpha1/baremetalhost_types_test.go
index 815dccdc..5e1435a6 100644
--- a/apis/metal3.io/v1alpha1/baremetalhost_types_test.go
+++ b/apis/metal3.io/v1alpha1/baremetalhost_types_test.go
@@ -317,10 +317,9 @@ func TestCredentialStatusMatch(t *testing.T) {
 
 func TestGetImageChecksum(t *testing.T) {
 	for _, tc := range []struct {
-		Scenario     string
-		Host         BareMetalHost
-		Expected     bool
-		ExpectedType string
+		Scenario string
+		Host     BareMetalHost
+		Expected bool
 	}{
 		{
 			Scenario: "both checksum value and type specified",
@@ -336,8 +335,7 @@ func TestGetImageChecksum(t *testing.T) {
 					},
 				},
 			},
-			Expected:     true,
-			ExpectedType: "md5",
+			Expected: true,
 		},
 		{
 			Scenario: "checksum value specified but not type",
@@ -352,25 +350,7 @@ func TestGetImageChecksum(t *testing.T) {
 					},
 				},
 			},
-			Expected:     true,
-			ExpectedType: "md5",
-		},
-		{
-			Scenario: "checksum value specified, auto type",
-			Host: BareMetalHost{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "myhost",
-					Namespace: "myns",
-				},
-				Spec: BareMetalHostSpec{
-					Image: &Image{
-						Checksum:     "md5hash",
-						ChecksumType: AutoChecksum,
-					},
-				},
-			},
-			Expected:     true,
-			ExpectedType: "",
+			Expected: true,
 		},
 		{
 			Scenario: "sha256 checksum value and type specified",
@@ -386,8 +366,7 @@ func TestGetImageChecksum(t *testing.T) {
 					},
 				},
 			},
-			Expected:     true,
-			ExpectedType: "sha256",
+			Expected: true,
 		},
 		{
 			Scenario: "sha512 checksum value and type specified",
@@ -403,8 +382,7 @@ func TestGetImageChecksum(t *testing.T) {
 					},
 				},
 			},
-			Expected:     true,
-			ExpectedType: "sha512",
+			Expected: true,
 		},
 		{
 			Scenario: "checksum value not specified",
@@ -465,10 +443,9 @@ func TestGetImageChecksum(t *testing.T) {
 		},
 	} {
 		t.Run(tc.Scenario, func(t *testing.T) {
-			_, checksumType, actual := tc.Host.Spec.Image.GetChecksum()
-			assert.Equal(t, tc.Expected, actual)
-			if tc.Expected {
-				assert.Equal(t, tc.ExpectedType, checksumType)
+			_, _, actual := tc.Host.Spec.Image.GetChecksum()
+			if actual != tc.Expected {
+				t.Errorf("expected %v but got %v", tc.Expected, actual)
 			}
 		})
 	}
diff --git a/hack/ci-e2e.sh b/hack/ci-e2e.sh
index 5597c5b1..d30156fc 100755
--- a/hack/ci-e2e.sh
+++ b/hack/ci-e2e.sh
@@ -116,17 +116,18 @@ IMAGE_FILE="cirros-${CIRROS_VERSION}-x86_64-disk.img"
 export IMAGE_CHECKSUM="c8fc807773e5354afe61636071771906"
 export IMAGE_URL="http://${IP_ADDRESS}/${IMAGE_FILE}"
 IMAGE_DIR="${REPO_ROOT}/test/e2e/images"
+mkdir -p "${IMAGE_DIR}"
 
 ## Download and run image server
-mkdir -p "${IMAGE_DIR}"
-pushd "${IMAGE_DIR}"
-wget --quiet https://artifactory.nordix.org/artifactory/metal3/images/iso/"${IMAGE_FILE}"
-popd
+wget --quiet -P "${IMAGE_DIR}"/ https://artifactory.nordix.org/artifactory/metal3/images/iso/"${IMAGE_FILE}"
 
 docker run --name image-server-e2e -d \
   -p 80:8080 \
   -v "${IMAGE_DIR}:/usr/share/nginx/html" nginxinc/nginx-unprivileged
 
+# Generate the key pair
+ssh-keygen -t ed25519 -f "${IMAGE_DIR}/ssh_testkey" -q -N ""
+
 # Generate credentials
 BMO_OVERLAY="${REPO_ROOT}/config/overlays/e2e"
 IRONIC_OVERLAY="${REPO_ROOT}/ironic-deployment/overlays/e2e"
diff --git a/pkg/provisioner/ironic/provision_test.go b/pkg/provisioner/ironic/provision_test.go
index a859a222..121b0d99 100644
--- a/pkg/provisioner/ironic/provision_test.go
+++ b/pkg/provisioner/ironic/provision_test.go
@@ -343,20 +343,6 @@ func TestIronicHasSameImage(t *testing.T) {
 			hostChecksum:     "thechecksum",
 			hostChecksumType: v1alpha1.MD5,
 		},
-		{
-			name:      "image same - auto checksum",
-			expected:  true,
-			liveImage: false,
-			node: nodes.Node{
-				InstanceInfo: map[string]interface{}{
-					"image_source":   "theimage",
-					"image_checksum": "thechecksum",
-				},
-			},
-			hostImage:        "theimage",
-			hostChecksum:     "thechecksum",
-			hostChecksumType: "auto",
-		},
 		{
 			name:      "image different",
 			expected:  false,
@@ -387,21 +373,6 @@ func TestIronicHasSameImage(t *testing.T) {
 			hostChecksum:     "different",
 			hostChecksumType: v1alpha1.MD5,
 		},
-		{
-			name:      "image checksum changed to auto",
-			expected:  false,
-			liveImage: false,
-			node: nodes.Node{
-				InstanceInfo: map[string]interface{}{
-					"image_source":        "theimage",
-					"image_os_hash_value": "thechecksum",
-					"image_os_hash_algo":  "md5",
-				},
-			},
-			hostImage:        "theimage",
-			hostChecksum:     "thechecksum",
-			hostChecksumType: "auto",
-		},
 		{
 			name:      "image checksum type different",
 			expected:  false,
diff --git a/pkg/provisioner/ironic/validatemanagementaccess_test.go b/pkg/provisioner/ironic/validatemanagementaccess_test.go
index 072e58b5..eb1f99e6 100644
--- a/pkg/provisioner/ironic/validatemanagementaccess_test.go
+++ b/pkg/provisioner/ironic/validatemanagementaccess_test.go
@@ -153,7 +153,6 @@ func TestValidateManagementAccessCreateWithImage(t *testing.T) {
 	host.Status.Provisioning.ID = "" // so we don't lookup by uuid
 	host.Spec.Image.URL = "theimagefoo"
 	host.Spec.Image.Checksum = "thechecksumxyz"
-	host.Spec.Image.ChecksumType = "auto"
 
 	var createdNode *nodes.Node
 
@@ -392,35 +391,14 @@ func TestValidateManagementAccessExistingSteadyStateNoUpdate(t *testing.T) {
 		},
 		{
 			Image: &metal3api.Image{
-				URL:          "theimage",
-				Checksum:     "thechecksum",
-				ChecksumType: "auto",
-			},
-			InstanceInfo: map[string]interface{}{
-				"image_source":   "theimage",
-				"image_checksum": "thechecksum",
-				"capabilities":   map[string]interface{}{},
-			},
-			DriverInfo: map[string]interface{}{
-				"force_persistent_boot_device": "Default",
-				"deploy_kernel":                "http://deploy.test/ipa.kernel",
-				"deploy_ramdisk":               "http://deploy.test/ipa.initramfs",
-				"test_address":                 "test.bmc",
-				"test_username":                "",
-				"test_password":                "******", // ironic returns a placeholder
-				"test_port":                    "42",
-			},
-		},
-		{
-			Image: &metal3api.Image{
-				URL:          "theimage",
-				Checksum:     "thechecksum",
-				ChecksumType: "sha256",
+				URL:      "theimage",
+				Checksum: "thechecksum",
 			},
 			InstanceInfo: map[string]interface{}{
 				"image_source":        "theimage",
-				"image_os_hash_algo":  "sha256",
+				"image_os_hash_algo":  "md5",
 				"image_os_hash_value": "thechecksum",
+				"image_checksum":      "thechecksum",
 				"capabilities":        map[string]interface{}{},
 			},
 			DriverInfo: map[string]interface{}{
diff --git a/test/e2e/basic_ops_test.go b/test/e2e/basic_ops_test.go
index a967a908..e06a9656 100644
--- a/test/e2e/basic_ops_test.go
+++ b/test/e2e/basic_ops_test.go
@@ -48,7 +48,11 @@ var _ = Describe("basic", func() {
 
 	It("should control power cycle of BMH though annotations", func() {
 		By("creating a secret with BMH credentials")
-		CreateBMHCredentialsSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcUser, bmcPassword)
+		bmcCredentialsData := map[string]string{
+			"username": bmcUser,
+			"password": bmcPassword,
+		}
+		CreateSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcCredentialsData)
 
 		By("creating a BMH")
 		bmh := metal3api.BareMetalHost{
diff --git a/test/e2e/basic_provisioning_test.go b/test/e2e/basic_provisioning_test.go
index 86a88b9f..ad5f2e04 100644
--- a/test/e2e/basic_provisioning_test.go
+++ b/test/e2e/basic_provisioning_test.go
@@ -4,8 +4,11 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"os"
 	"time"
 
+	"golang.org/x/crypto/ssh"
+
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
@@ -17,6 +20,8 @@ import (
 	"sigs.k8s.io/cluster-api/util/patch"
 
 	metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
+
+	capm3_e2e "github.com/metal3-io/cluster-api-provider-metal3/test/e2e"
 )
 
 var _ = Describe("BMH Provisioning and Annotation Management", func() {
@@ -47,7 +52,11 @@ var _ = Describe("BMH Provisioning and Annotation Management", func() {
 
 	It("provisions a BMH, applies detached and status annotations, then deprovisions", func() {
 		By("Creating a secret with BMH credentials")
-		CreateBMHCredentialsSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcUser, bmcPassword)
+		bmcCredentialsData := map[string]string{
+			"username": bmcUser,
+			"password": bmcPassword,
+		}
+		CreateSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcCredentialsData)
 
 		By("Creating a BMH with inspection disabled and hardware details added")
 		bmh := metal3api.BareMetalHost{
@@ -65,8 +74,9 @@ var _ = Describe("BMH Provisioning and Annotation Management", func() {
 					Address:         bmcAddress,
 					CredentialsName: "bmc-credentials",
 				},
-				BootMode:       metal3api.Legacy,
-				BootMACAddress: bootMacAddress,
+				BootMode:              metal3api.Legacy,
+				BootMACAddress:        bootMacAddress,
+				AutomatedCleaningMode: "disabled",
 			},
 		}
 		err := clusterProxy.GetClient().Create(ctx, &bmh)
@@ -83,13 +93,22 @@ var _ = Describe("BMH Provisioning and Annotation Management", func() {
 		helper, err := patch.NewHelper(&bmh, clusterProxy.GetClient())
 		Expect(err).NotTo(HaveOccurred())
 		bmh.Spec.Image = &metal3api.Image{
-			URL:          e2eConfig.GetVariable("IMAGE_URL"),
-			Checksum:     e2eConfig.GetVariable("IMAGE_CHECKSUM"),
-			ChecksumType: metal3api.AutoChecksum,
+			URL:      e2eConfig.GetVariable("IMAGE_URL"),
+			Checksum: e2eConfig.GetVariable("IMAGE_CHECKSUM"),
 		}
 		bmh.Spec.RootDeviceHints = &metal3api.RootDeviceHints{
 			DeviceName: "/dev/vda",
 		}
+		// The ssh check is not possible in all situations (e.g. fixture) so it can be skipped
+		if e2eConfig.GetVariable("SSH_CHECK_PROVISIONED") == "true" {
+			userDataSecretName := "user-data"
+			sshPubKeyPath := e2eConfig.GetVariable("SSH_PUB_KEY")
+			createCirrosInstanceAndHostnameUserdata(ctx, clusterProxy.GetClient(), namespace.Name, userDataSecretName, sshPubKeyPath)
+			bmh.Spec.UserData = &corev1.SecretReference{
+				Name:      userDataSecretName,
+				Namespace: namespace.Name,
+			}
+		}
 		Expect(helper.Patch(ctx, &bmh)).To(Succeed())
 
 		By("Waiting for the BMH to be in provisioning state")
@@ -106,6 +125,22 @@ var _ = Describe("BMH Provisioning and Annotation Management", func() {
 			State:  metal3api.StateProvisioned,
 		}, e2eConfig.GetIntervals(specName, "wait-provisioned")...)
 
+		// The ssh check is not possible in all situations (e.g. fixture) so it can be skipped
+		if e2eConfig.GetVariable("SSH_CHECK_PROVISIONED") == "true" {
+			By("Verifying the node booting from disk")
+			keyPath := e2eConfig.GetVariable("SSH_PRIV_KEY")
+			key, err := os.ReadFile(keyPath)
+			Expect(err).NotTo(HaveOccurred(), "unable to read private key")
+
+			signer, err := ssh.ParsePrivateKey(key)
+			Expect(err).NotTo(HaveOccurred(), "unable to parse private key")
+
+			auth := ssh.PublicKeys(signer)
+			PerformSSHBootCheck(e2eConfig, "disk", auth)
+		} else {
+			capm3_e2e.Logf("WARNING: Skipping SSH check since SSH_CHECK_PROVISIONED != true")
+		}
+
 		By("Retrieving the latest BMH object")
 		err = clusterProxy.GetClient().Get(ctx, types.NamespacedName{
 			Name:      bmh.Name,
@@ -157,7 +192,11 @@ var _ = Describe("BMH Provisioning and Annotation Management", func() {
 		}, e2eConfig.GetIntervals(specName, "wait-secret-deletion")...).Should(BeTrue())
 
 		By("Creating a secret with BMH credentials")
-		CreateBMHCredentialsSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcUser, bmcPassword)
+		bmcCredentialsData = map[string]string{
+			"username": bmcUser,
+			"password": bmcPassword,
+		}
+		CreateSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcCredentialsData)
 
 		By("Recreating the BMH with the previously saved status in the status annotation")
 		bmh = metal3api.BareMetalHost{
@@ -174,8 +213,9 @@ var _ = Describe("BMH Provisioning and Annotation Management", func() {
 					Address:         bmcAddress,
 					CredentialsName: "bmc-credentials",
 				},
-				BootMode:       metal3api.Legacy,
-				BootMACAddress: bootMacAddress,
+				BootMode:              metal3api.Legacy,
+				BootMACAddress:        bootMacAddress,
+				AutomatedCleaningMode: "disabled",
 				Image: &metal3api.Image{
 					URL:      e2eConfig.GetVariable("IMAGE_URL"),
 					Checksum: e2eConfig.GetVariable("IMAGE_CHECKSUM"),
diff --git a/test/e2e/common.go b/test/e2e/common.go
index 4e96b6ab..c559e538 100644
--- a/test/e2e/common.go
+++ b/test/e2e/common.go
@@ -154,7 +154,7 @@ func (c *Config) GetVariable(varName string) string {
 	}
 
 	value, ok := c.Variables[varName]
-	Expect(ok).To(BeTrue())
+	Expect(ok).To(BeTrue(), fmt.Sprintf("Configuration variable '%s' not found", varName))
 	return value
 }
 
@@ -281,24 +281,16 @@ func buildKustomizeManifest(source string) ([]byte, error) {
 	return resources.AsYaml()
 }
 
-func CreateBMHCredentialsSecret(ctx context.Context, client client.Client, secretNamespace, secretName, username, password string) error {
-	bmcCredentials := corev1.Secret{
+func CreateSecret(ctx context.Context, client client.Client, secretNamespace, secretName string, data map[string]string) {
+	secret := corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      secretName,
 			Namespace: secretNamespace,
 		},
-		StringData: map[string]string{
-			"username": username,
-			"password": password,
-		},
+		StringData: data,
 	}
 
-	err := client.Create(ctx, &bmcCredentials)
-	if err != nil {
-		return fmt.Errorf("failed to create BMC credentials secret: %w", err)
-	}
-
-	return nil
+	Expect(client.Create(ctx, &secret)).NotTo(HaveOccurred(), fmt.Sprintf("Failed to create secret '%s/%s'", secretNamespace, secretName))
 }
 
 func executeSSHCommand(client *ssh.Client, command string) (string, error) {
@@ -350,12 +342,72 @@ func IsBootedFromDisk(client *ssh.Client) (bool, error) {
 		return false, fmt.Errorf("error executing 'df -h': %w", err)
 	}
 
-	isDisk := HasRootOnDisk(output)
-	if isDisk {
+	bootedFromDisk := HasRootOnDisk(output)
+	if bootedFromDisk {
 		capm3_e2e.Logf("System is booted from a disk.")
 	} else {
 		capm3_e2e.Logf("System is booted from a live ISO.")
 	}
 
-	return isDisk, nil
+	return bootedFromDisk, nil
+}
+
+func EstablishSSHConnection(e2eConfig *Config, auth ssh.AuthMethod, user, address string) *ssh.Client {
+	config := &ssh.ClientConfig{
+		User:            user,
+		Auth:            []ssh.AuthMethod{auth},
+		HostKeyCallback: ssh.InsecureIgnoreHostKey(), // #nosec G106
+	}
+
+	var client *ssh.Client
+	var err error
+	Eventually(func() error {
+		client, err = ssh.Dial("tcp", address, config)
+		return err
+	}, e2eConfig.GetIntervals("default", "wait-connect-ssh")...).Should(Succeed(), "Failed to establish SSH connection")
+
+	return client
+}
+
+// createCirrosInstanceAndHostnameUserdata creates a Kubernetes secret intended for cloud-init usage.
+// This userdata is utilized during BMH's initialization and setup.
+func createCirrosInstanceAndHostnameUserdata(ctx context.Context, client client.Client, namespace string, secretName string, sshPubKeyPath string) {
+	sshPubKeyData, err := os.ReadFile(sshPubKeyPath) // #nosec G304
+	Expect(err).NotTo(HaveOccurred(), "Failed to read SSH public key file")
+
+	userDataContent := fmt.Sprintf(`#!/bin/sh
+mkdir /root/.ssh
+mkdir /home/cirros/.ssh
+chmod 700 /root/.ssh
+chmod 700 /home/cirros/.ssh
+chown cirros /home/cirros/.ssh
+echo "%s" >> /home/cirros/.ssh/authorized_keys
+echo "%s" >> /root/.ssh/authorized_keys`, sshPubKeyData, sshPubKeyData)
+
+	CreateSecret(ctx, client, namespace, secretName, map[string]string{"userData": userDataContent})
+}
+
+// PerformSSHBootCheck performs an SSH check to verify the node's boot source.
+// The `expectedBootMode` parameter should be "disk" or "memory".
+// The `auth` parameter is an ssh.AuthMethod for authentication.
+func PerformSSHBootCheck(e2eConfig *Config, expectedBootMode string, auth ssh.AuthMethod) {
+	ip := e2eConfig.GetVariable("IP_BMO_E2E_0")
+	sshPort := e2eConfig.GetVariable("SSH_PORT")
+	address := fmt.Sprintf("%s:%s", ip, sshPort)
+	user := e2eConfig.GetVariable("CIRROS_USERNAME")
+
+	client := EstablishSSHConnection(e2eConfig, auth, user, address)
+	defer func() {
+		if client != nil {
+			client.Close()
+		}
+	}()
+
+	bootedFromDisk, err := IsBootedFromDisk(client)
+	Expect(err).NotTo(HaveOccurred(), "Error in verifying boot mode")
+
+	// Compare actual boot source with expected
+	isExpectedBootMode := (expectedBootMode == "disk" && bootedFromDisk) ||
+		(expectedBootMode == "memory" && !bootedFromDisk)
+	Expect(isExpectedBootMode).To(BeTrue(), fmt.Sprintf("Expected booting from %s, but found different mode", expectedBootMode))
 }
diff --git a/test/e2e/config/ironic.yaml b/test/e2e/config/ironic.yaml
index b7d293a4..18252c81 100644
--- a/test/e2e/config/ironic.yaml
+++ b/test/e2e/config/ironic.yaml
@@ -35,6 +35,10 @@ variables:
   SSH_CHECK_PROVISIONED: "true"
   IP_BMO_E2E_0: "192.168.222.122"
   SSH_PORT: "22"
+  CIRROS_USERNAME: "cirros"
+  CIRROS_PASSWORD: "gocubsgo"
+  SSH_PRIV_KEY: "./images/ssh_testkey"
+  SSH_PUB_KEY: "./images/ssh_testkey.pub"
 
 intervals:
   inspection/wait-unmanaged: ["1m", "5s"]
diff --git a/test/e2e/external_inspection_test.go b/test/e2e/external_inspection_test.go
index 5de4f24b..03a494a5 100644
--- a/test/e2e/external_inspection_test.go
+++ b/test/e2e/external_inspection_test.go
@@ -197,8 +197,12 @@ var _ = Describe("External Inspection", func() {
 	})
 
 	It("should skip inspection and become available when a BMH has annotations with hardware details and inspection disabled", func() {
-		By("creating a secret with BMH credentials")
-		CreateBMHCredentialsSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcUser, bmcPassword)
+		By("Creating a secret with BMH credentials")
+		bmcCredentialsData := map[string]string{
+			"username": bmcUser,
+			"password": bmcPassword,
+		}
+		CreateSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcCredentialsData)
 
 		By("creating a BMH with inspection disabled and hardware details added")
 		bmh := metal3api.BareMetalHost{
diff --git a/test/e2e/inspection_test.go b/test/e2e/inspection_test.go
index 9f1a6046..552aabd7 100644
--- a/test/e2e/inspection_test.go
+++ b/test/e2e/inspection_test.go
@@ -92,8 +92,12 @@ var _ = Describe("Inspection", func() {
 	})
 
 	It("should inspect a newly created BMH", func() {
-		By("creating a secret with BMH credentials")
-		CreateBMHCredentialsSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcUser, bmcPassword)
+		By("Creating a secret with BMH credentials")
+		bmcCredentialsData := map[string]string{
+			"username": bmcUser,
+			"password": bmcPassword,
+		}
+		CreateSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcCredentialsData)
 
 		By("creating a BMH")
 		bmh := metal3api.BareMetalHost{
diff --git a/test/e2e/live_iso_test.go b/test/e2e/live_iso_test.go
index 7a02e9ac..fc8bb3a2 100644
--- a/test/e2e/live_iso_test.go
+++ b/test/e2e/live_iso_test.go
@@ -4,9 +4,10 @@ import (
 	"context"
 	"fmt"
 
+	"golang.org/x/crypto/ssh"
+
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
-	"golang.org/x/crypto/ssh"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/pointer"
@@ -49,7 +50,11 @@ var _ = Describe("Live-ISO", func() {
 
 	It("should provision a BMH with live ISO and then deprovision it", func() {
 		By("Creating a secret with BMH credentials")
-		CreateBMHCredentialsSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcUser, bmcPassword)
+		bmcCredentialsData := map[string]string{
+			"username": bmcUser,
+			"password": bmcPassword,
+		}
+		CreateSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcCredentialsData)
 
 		By("Creating a BMH with inspection disabled and hardware details added")
 		bmh := metal3api.BareMetalHost{
@@ -71,12 +76,8 @@ var _ = Describe("Live-ISO", func() {
 					URL:        imageURL,
 					DiskFormat: pointer.String("live-iso"),
 				},
-				BootMode:              metal3api.Legacy,
-				BootMACAddress:        bootMacAddress,
-				AutomatedCleaningMode: "disabled",
-				RootDeviceHints: &metal3api.RootDeviceHints{
-					DeviceName: "/dev/vda",
-				},
+				BootMode:       metal3api.Legacy,
+				BootMACAddress: bootMacAddress,
 			},
 		}
 		err := clusterProxy.GetClient().Create(ctx, &bmh)
@@ -99,39 +100,11 @@ var _ = Describe("Live-ISO", func() {
 		// The ssh check is not possible in all situations (e.g. fixture) so it can be skipped
 		if e2eConfig.GetVariable("SSH_CHECK_PROVISIONED") == "true" {
 			By("Verifying the node booted from live ISO image")
-
-			// Set up SSH client configuration
-			config := &ssh.ClientConfig{
-				User: "cirros",
-				Auth: []ssh.AuthMethod{
-					ssh.Password("gocubsgo"),
-				},
-				HostKeyCallback: ssh.InsecureIgnoreHostKey(), // #nosec G106
-			}
-
-			ip := e2eConfig.GetVariable("IP_BMO_E2E_0")
-			sshPort := e2eConfig.GetVariable("SSH_PORT")
-			address := fmt.Sprintf("%s:%s", ip, sshPort)
-
-			// Establish an SSH connection
-			var client *ssh.Client
-			var err error
-
-			Eventually(func() error {
-				client, err = ssh.Dial("tcp", address, config)
-				return err
-			}, e2eConfig.GetIntervals(specName, "wait-connect-ssh")...).Should(Succeed(), "Failed to establish SSH connection")
-
-			defer func() {
-				if client != nil {
-					client.Close()
-				}
-			}()
-			isDisk, err := IsBootedFromDisk(client)
-			Expect(err).NotTo(HaveOccurred(), "Error in verifying boot mode")
-			Expect(isDisk).To(Equal(false), "Error booting from disk when live ISO is expected")
+			password := e2eConfig.GetVariable("CIRROS_PASSWORD")
+			auth := ssh.Password(password)
+			PerformSSHBootCheck(e2eConfig, "memory", auth)
 		} else {
-			capm3_e2e.Logf("WARNING: Skipping ssh check since SSH_CHECK_PROVISIONED != true")
+			capm3_e2e.Logf("WARNING: Skipping SSH check since SSH_CHECK_PROVISIONED != true")
 		}
 
 		By("Triggering the deprovisioning of the BMH")
diff --git a/test/e2e/re_inspection_test.go b/test/e2e/re_inspection_test.go
index d9b64986..1b1f469c 100644
--- a/test/e2e/re_inspection_test.go
+++ b/test/e2e/re_inspection_test.go
@@ -50,8 +50,12 @@ var _ = Describe("Re-Inspection", func() {
 	})
 
 	It("should re-inspect the annotated BMH", func() {
-		By("creating a secret with BMH credentials")
-		CreateBMHCredentialsSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcUser, bmcPassword)
+		By("Creating a secret with BMH credentials")
+		bmcCredentialsData := map[string]string{
+			"username": bmcUser,
+			"password": bmcPassword,
+		}
+		CreateSecret(ctx, clusterProxy.GetClient(), namespace.Name, secretName, bmcCredentialsData)
 
 		By("creating a BMH with inspection disabled and hardware details added with wrong HostName")
 		newHardwareDetails := strings.Replace(hardwareDetails, "bmo-e2e-0", wrongHostName, 1)
diff --git a/test/go.mod b/test/go.mod
index d0a5de89..3249f27d 100644
--- a/test/go.mod
+++ b/test/go.mod
@@ -68,7 +68,7 @@ require (
 	github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
 	github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
-	github.com/google/uuid v1.5.0 // indirect
+	github.com/google/uuid v1.4.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/huandu/xstrings v1.4.0 // indirect
 	github.com/imdario/mergo v0.3.13 // indirect
diff --git a/test/go.sum b/test/go.sum
index 2098a2d0..41403b47 100644
--- a/test/go.sum
+++ b/test/go.sum
@@ -250,8 +250,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
-github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
+github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=