Repository: syself/cluster-api-provider-hetzner
Pull request #1406 (2024-07-29 14:16:03)

:seedling: fix readme links
<!-- If this is your first PR, welcome! Please make sure you read the [contributing guidelines](../CONTRIBUTING.md#contributing-a-patch). -->
<!-- please add an icon to the title of this PR and delete this line and similar ones -->
<!-- the icon will be either ⚠️ (:warning:, major or breaking changes), ✨ (:sparkles:, feature additions), 🐛 (:bug:, patches and bugfixes), 📖 (:book:, documentation or proposals), or 🌱 (:seedling:, minor or other) -->

**What this PR does / why we need it**:

Fix the broken links on the README. Now all links point to the correct files/folders.

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #

**Special notes for your reviewer**:

_Please confirm that if this PR changes any image versions, then that's the sole change this PR makes._

**TODOs**:

- [x] squash commits
- [ ] include documentation
- [ ] add unit tests




Patch:
diff --git a/.envrc.sample b/.envrc.sample
index e334dd89..987366eb 100644
--- a/.envrc.sample
+++ b/.envrc.sample
@@ -1,8 +1,8 @@
 export TTS_TOKEN=...
-export CLUSTER_NAME=test
+export CLUSTER_NAME=caph-${USER}
 export KUBECONFIG=$PWD/.mgt-cluster-kubeconfig.yaml
 export HCLOUD_TOKEN=...
-export HCLOUD_SSH_KEY=test
+export SSH_KEY_NAME=my-caph-ssh-key
 export HCLOUD_REGION=fsn1
 export CONTROL_PLANE_MACHINE_COUNT=1
 export WORKER_MACHINE_COUNT=1
@@ -10,8 +10,8 @@ export KUBERNETES_VERSION=v1.29.4
 export HCLOUD_IMAGE_NAME=1.29.4-ubuntu-22.04-containerd
 export HCLOUD_CONTROL_PLANE_MACHINE_TYPE=cpx31
 export HCLOUD_WORKER_MACHINE_TYPE=cpx31
-export SSH_KEY=$HOME/.ssh/id_rsa.pub
-export HETZNER_SSH_PUB_PATH=$HOME/.ssh/id_rsa.pub
-export HETZNER_SSH_PRIV_PATH=$HOME/.ssh/id_rsa
+export SSH_KEY=$HOME/.ssh/my-caph-ssh-key.pub
+export HETZNER_SSH_PUB_PATH=$HOME/.ssh/my-caph-ssh-key.pub
+export HETZNER_SSH_PRIV_PATH=$HOME/.ssh/my-caph-ssh-key
 export HETZNER_ROBOT_USER=
 export HETZNER_ROBOT_PASSWORD=
diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml
new file mode 100644
index 00000000..76f35925
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yaml
@@ -0,0 +1,5 @@
+blank_issues_enabled: true
+contact_links:
+  - name: Other support requests and sales questions
+    url: https://syself.com/contact#sales
+    about: Contact us — we're happy to help!
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 2e9f6940..080a9824 100644
--- a/Makefile
+++ b/Makefile
@@ -212,18 +212,18 @@ install-ccm-in-wl-cluster:
 	@echo 'run "kubectl --kubeconfig=$(WORKER_CLUSTER_KUBECONFIG) ..." to work with the new target cluster'
 
 add-ssh-pub-key:
-	./hack/ensure-env-variables.sh HCLOUD_TOKEN SSH_KEY HCLOUD_SSH_KEY
+	./hack/ensure-env-variables.sh HCLOUD_TOKEN SSH_KEY SSH_KEY_NAME
 	SSH_KEY_CONTENT=$$(cat $(SSH_KEY)) ; \
 	curl -sS \
 		-X POST \
 		-H "Authorization: Bearer $${HCLOUD_TOKEN}" \
 		-H "Content-Type: application/json" \
-		-d '{"labels":{},"name":"${HCLOUD_SSH_KEY}","public_key":"'"$${SSH_KEY_CONTENT}"'"}' \
+		-d '{"labels":{},"name":"${SSH_KEY_NAME}","public_key":"'"$${SSH_KEY_CONTENT}"'"}' \
 		'https://api.hetzner.cloud/v1/ssh_keys'
 
 env-vars-for-wl-cluster:
 	@./hack/ensure-env-variables.sh CLUSTER_NAME CONTROL_PLANE_MACHINE_COUNT HCLOUD_CONTROL_PLANE_MACHINE_TYPE \
-	HCLOUD_REGION HCLOUD_SSH_KEY HCLOUD_WORKER_MACHINE_TYPE KUBERNETES_VERSION WORKER_MACHINE_COUNT
+	HCLOUD_REGION SSH_KEY_NAME HCLOUD_WORKER_MACHINE_TYPE KUBERNETES_VERSION WORKER_MACHINE_COUNT
 
 create-workload-cluster-hcloud: env-vars-for-wl-cluster $(KUSTOMIZE) $(ENVSUBST) install-crds ## Creates a workload-cluster.
 	# Create workload Cluster.
@@ -267,9 +267,9 @@ create-workload-cluster-hcloud-network-packer: env-vars-for-wl-cluster $(KUSTOMI
 
 create-workload-cluster-hetzner-hcloud-control-plane: env-vars-for-wl-cluster $(KUSTOMIZE) $(ENVSUBST) ## Creates a workload-cluster.
 	# Create workload Cluster.
-	./hack/ensure-env-variables.sh HCLOUD_TOKEN HETZNER_ROBOT_USER HETZNER_ROBOT_PASSWORD HETZNER_SSH_PRIV_PATH HETZNER_SSH_PUB_PATH
+	./hack/ensure-env-variables.sh HCLOUD_TOKEN HETZNER_ROBOT_USER HETZNER_ROBOT_PASSWORD HETZNER_SSH_PRIV_PATH HETZNER_SSH_PUB_PATH SSH_KEY_NAME
 	$(KUBECTL) create secret generic $(INFRA_PROVIDER) --from-literal=hcloud=$(HCLOUD_TOKEN) --from-literal=robot-user=$(HETZNER_ROBOT_USER) --from-literal=robot-password=$(HETZNER_ROBOT_PASSWORD) --save-config --dry-run=client -o yaml | $(KUBECTL) apply -f -
-	$(KUBECTL) create secret generic robot-ssh --from-literal=sshkey-name=test --from-file=ssh-privatekey=${HETZNER_SSH_PRIV_PATH} --from-file=ssh-publickey=${HETZNER_SSH_PUB_PATH} --save-config --dry-run=client -o yaml | $(KUBECTL) apply -f -
+	$(KUBECTL) create secret generic robot-ssh --from-literal=sshkey-name=$(SSH_KEY_NAME) --from-file=ssh-privatekey=${HETZNER_SSH_PRIV_PATH} --from-file=ssh-publickey=${HETZNER_SSH_PUB_PATH} --save-config --dry-run=client -o yaml | $(KUBECTL) apply -f -
 	$(KUSTOMIZE) build templates/cluster-templates/$(INFRA_PROVIDER)-hcloud-control-planes --load-restrictor LoadRestrictionsNone  > templates/cluster-templates/cluster-template-$(INFRA_PROVIDER)-hcloud-control-planes.yaml
 	cat templates/cluster-templates/cluster-template-$(INFRA_PROVIDER)-hcloud-control-planes.yaml | $(ENVSUBST) - | $(KUBECTL) apply -f -
 	$(MAKE) wait-and-get-secret
@@ -278,9 +278,9 @@ create-workload-cluster-hetzner-hcloud-control-plane: env-vars-for-wl-cluster $(
 
 create-workload-cluster-hetzner-baremetal-control-plane: env-vars-for-wl-cluster $(KUSTOMIZE) $(ENVSUBST) ## Creates a workload-cluster.
 	# Create workload Cluster.
-	./hack/ensure-env-variables.sh HCLOUD_TOKEN HETZNER_ROBOT_USER HETZNER_ROBOT_PASSWORD HETZNER_SSH_PRIV_PATH HETZNER_SSH_PUB_PATH
+	./hack/ensure-env-variables.sh HCLOUD_TOKEN HETZNER_ROBOT_USER HETZNER_ROBOT_PASSWORD HETZNER_SSH_PRIV_PATH HETZNER_SSH_PUB_PATH SSH_KEY_NAME
 	$(KUBECTL) create secret generic $(INFRA_PROVIDER) --from-literal=hcloud=$(HCLOUD_TOKEN) --from-literal=robot-user=$(HETZNER_ROBOT_USER) --from-literal=robot-password=$(HETZNER_ROBOT_PASSWORD) --save-config --dry-run=client -o yaml | $(KUBECTL) apply -f -
-	$(KUBECTL) create secret generic robot-ssh --from-literal=sshkey-name=test --from-file=ssh-privatekey=${HETZNER_SSH_PRIV_PATH} --from-file=ssh-publickey=${HETZNER_SSH_PUB_PATH} --save-config --dry-run=client -o yaml | $(KUBECTL) apply -f -
+	$(KUBECTL) create secret generic robot-ssh --from-literal=sshkey-name=$(SSH_KEY_NAME) --from-file=ssh-privatekey=${HETZNER_SSH_PRIV_PATH} --from-file=ssh-publickey=${HETZNER_SSH_PUB_PATH} --save-config --dry-run=client -o yaml | $(KUBECTL) apply -f -
 	$(KUSTOMIZE) build templates/cluster-templates/$(INFRA_PROVIDER)-baremetal-control-planes --load-restrictor LoadRestrictionsNone  > templates/cluster-templates/cluster-template-$(INFRA_PROVIDER)-baremetal-control-planes.yaml
 	cat templates/cluster-templates/cluster-template-$(INFRA_PROVIDER)-baremetal-control-planes.yaml | $(ENVSUBST) - | $(KUBECTL) apply -f -
 	$(MAKE) wait-and-get-secret
@@ -289,9 +289,9 @@ create-workload-cluster-hetzner-baremetal-control-plane: env-vars-for-wl-cluster
 
 create-workload-cluster-hetzner-baremetal-control-plane-remediation: env-vars-for-wl-cluster $(KUSTOMIZE) $(ENVSUBST) ## Creates a workload-cluster.
 	# Create workload Cluster.
-	./hack/ensure-env-variables.sh HCLOUD_TOKEN HETZNER_ROBOT_USER HETZNER_ROBOT_PASSWORD HETZNER_SSH_PRIV_PATH HETZNER_SSH_PUB_PATH
+	./hack/ensure-env-variables.sh HCLOUD_TOKEN HETZNER_ROBOT_USER HETZNER_ROBOT_PASSWORD HETZNER_SSH_PRIV_PATH HETZNER_SSH_PUB_PATH SSH_KEY_NAME
 	$(KUBECTL) create secret generic $(INFRA_PROVIDER) --from-literal=hcloud=$(HCLOUD_TOKEN) --from-literal=robot-user=$(HETZNER_ROBOT_USER) --from-literal=robot-password=$(HETZNER_ROBOT_PASSWORD) --save-config --dry-run=client -o yaml | $(KUBECTL) apply -f -
-	$(KUBECTL) create secret generic robot-ssh --from-literal=sshkey-name=test --from-file=ssh-privatekey=${HETZNER_SSH_PRIV_PATH} --from-file=ssh-publickey=${HETZNER_SSH_PUB_PATH} --save-config --dry-run=client -o yaml | $(KUBECTL) apply -f -
+	$(KUBECTL) create secret generic robot-ssh --from-literal=sshkey-name=$(SSH_KEY_NAME) --from-file=ssh-privatekey=${HETZNER_SSH_PRIV_PATH} --from-file=ssh-publickey=${HETZNER_SSH_PUB_PATH} --save-config --dry-run=client -o yaml | $(KUBECTL) apply -f -
 	$(KUSTOMIZE) build templates/cluster-templates/$(INFRA_PROVIDER)-baremetal-control-planes-remediation --load-restrictor LoadRestrictionsNone  > templates/cluster-templates/cluster-template-$(INFRA_PROVIDER)-baremetal-control-planes-remediation.yaml
 	cat templates/cluster-templates/cluster-template-$(INFRA_PROVIDER)-baremetal-control-planes-remediation.yaml | $(ENVSUBST) - | $(KUBECTL) apply -f -
 	$(MAKE) wait-and-get-secret
diff --git a/README.md b/README.md
index 67e1e145..7231add6 100644
--- a/README.md
+++ b/README.md
@@ -5,10 +5,12 @@
 <div align="center">
 <a href="https://syself.com/docs/caph/getting-started/quickstart">Quickstart</a> |
 <a href="https://syself.com/docs/caph/getting-started/introduction">Docs</a> |
-<a href="https://syself.com/docs/caph/developers/development-guide">Contribution Guide</a><br><br>
-<a href="https://cluster-api.sigs.k8s.io/">Cluster API Book</a>
+<a href="https://cluster-api.sigs.k8s.io/">Cluster API Book</a><br><br>
+<p>⭐ Consider leaving a star — it motivates us a lot! ⭐</p>
 </div>
 
+
+
 ---
 
 <div align="center">
@@ -22,60 +24,43 @@
 
 <br>
 
-The Kubernetes Cluster API Provider Hetzner (CAPH) enables declarative provisioning of multiple Kubernetes clusters on [Hetzner infrastructure](https://hetzner.cloud).
+## Table of Contents
+- [What is CAPH](#-what-is-the-cluster-api-provider-hetzner)
+- [Documentation](#-documentation)
+- [Getting Started](#-getting-started)
+- [Version Compatibility](#%EF%B8%8F-compatibility-with-cluster-api-and-kubernetes-versions)
+- [Node Images](#-operating-system-images)
+- [Contributing](#-getting-involved-and-contributing)
+- [Contact](#-contact)
 
-With CAPH, you can manage highly-available Kubernetes clusters on both bare metal and cloud instances, leveraging the Cluster API to handle creation, updates, and operations of production-ready, self-managed Kubernetes clusters at any scale.
+## 📰 What is the Cluster API Provider Hetzner?
 
 > [!NOTE]
 > The Cluster API Provider Hetzner is independently maintained by [Syself](https://syself.com) and the community. It is not an official Hetzner project.
 >
 > If you have any questions about this project, please start a conversation in the [Discussions](https://github.com/syself/cluster-api-provider-hetzner/discussions) tab or contact us at [contact@syself.com](mailto:contact@syself.com?subject=cluster-api-provider-hetzner).
 
-## 📰 What is the Cluster API Provider Hetzner?
-
-The [Cluster API][cluster_api] orchestrates infrastructure similarly to how Kubernetes manages containers. It implements a declarative API like Kubernetes does and extends the resources of the Kubernetes API server via CRDs.
+The Cluster API Provider Hetzner (CAPH) provides a way to declaratively create and manage infrastructure on Hetzner, in a Kubernetes-native way. It extends the Kubernetes API with Custom Resource Definitions (CRDs) allowing you to interact with clusters in the same fashion you interact with workload.
 
-The Cluster API consists of the CAPI controller, the control-plane provider, the bootstrap provider, and an infrastructure provider like CAPH, that translates resources in Hetzner to objects in the Kubernetes API.
+Key benefits include:
 
-The controllers ensure that the desired state of the infrastructure is achieved - just as Kubernetes ensures the desired state of containers. The concept of [Kubernetes Controller](https://kubernetes.io/docs/concepts/architecture/controller/) has significant advantages over traditional Infrastructure as Code (IaC) solutions because it can react automatically to changes and problems. The best example of this is the MachineHealthCheck, which replaces unhealthy nodes automatically.
+- **Self-healing**: CAPH and CAPI controllers react to every change in your infrastructure, identifying and resolving issues without human intervention
+- **Declarative**: Specify the desired state of your infrastructure and let the operators do the rest, ensuring repeatability and idempotency
+- **Kubernetes native**: Everything is a Kubernetes resource, meaning you can use tools you're already familiar with while working with CAPH
 
-Using CAPH unites the benefits of declarative infrastructure, cost-effectiveness, and GDPR-compliant European cloud, ensuring that your clusters can automatically adapt to changes and problems.
+CAPH enables you to have DIY Kubernetes on Hetzner at any scale, with full control over your infrastructure and clusters configuration.
 
-## ✨ Features of CAPH
+If you want a batteries-included solution instead, you can try [Syself](https://syself.com) free for 14 days.
 
-- Native Kubernetes resources and API
-- Works with your choice of Linux distribution
-- Support for single and multi-node control plane clusters (HA Kubernetes)
-- Support for Hetzner Cloud placement groups, network, and load balancer
-- Complete day 2 operations - updating Kubernetes and nodes, scaling up and down, self-healing
-- Custom CSR approver for approving [kubelet-serving certificate signing requests](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-certs/#kubelet-serving-certs)
-- Hetzner dedicated servers / bare metal (and GPUs)
-
-## 👀 Clarifying Scope
-
-Managing a production-grade Kubernetes system requires a **dedicated team of experts**.
-
-The Cluster API Provider Hetzner (CAPH) handles the lifecycle management of machines and infrastructure, but certain aspects need to be managed separately:
-
-- ❌ Production-ready node images
-- ❌ Secured kubeadm configuration
-- ❌ Incorporation of cluster add-ons, such as CNI (e.g. cilium), metrics-server, konnectivity-service, etc.
-- ❌ Testing & update procedures of Kubernetes version, configuration
-- ❌ Backup procedures
-- ❌ Monitoring strategies
-- ❌ Alerting systems
-- ❌ Identity and Access Management (IAM)
+## 📖 Documentation
 
-If you don't have a dedicated team for managing Kubernetes, you can use [Syself Autopilot](https://syself.com) and enjoy a wide range of benefits, including:
+Documentation can be found at [caph.syself.com](https://caph.syself.com). You can contribute to it by modifying the contents of the `/docs` directory.
 
-- ✅ Consistent, regular updates that provide you with the latest features and improvements.
-- ✅ Highly optimized defaults, reducing costs by up to 80% without performance impacts.
-- ✅ Production-ready clusters working out of the box.
-- ✅ Specialized expertise in Cluster API and Hetzner for quick issue resolution and 24/7 support.
+## 🚀 Getting Started
 
-## 🚀 Get Started
+The best way to get started with CAPH is to spin up a cluster. For that you can follow our [**Managing Kubernetes on Hetzner with Cluster API**](https://community.hetzner.com/tutorials/kubernetes-on-hetzner-with-cluster-api) article featured in the Hetzner Community Tutorials.
 
-Ready to dive in? Here are some resources to get you started:
+Additional resources from the documentation:
 
 - [**Cluster API Provider Hetzner 15 Minute Tutorial**](https://syself.com/docs/caph/getting-started/quickstart): Set up a bootstrap cluster using Kind and deploy a Kubernetes cluster on Hetzner.
 - [**Develop and test Kubernetes clusters with Tilt**](https://syself.com/docs/caph/developers/development-guide): Start using Tilt for rapid testing of various cluster flavors, like with/without a private network or bare metal.
@@ -83,6 +68,7 @@ Ready to dive in? Here are some resources to get you started:
 
 In addition to the pure creation and operation of Kubernetes clusters, this provider can also validate and approve certificate signing requests. This increases security as the kubelets of the nodes can be operated with signed certificates, and enables the metrics-server to run securely. [Click here](https://syself.com/docs/caph/topics/advanced/csr-controller) to read more about the CSR controller.
 
+
 ## 🖇️ Compatibility with Cluster API and Kubernetes Versions
 
 This provider's versions are compatible with the following versions of Cluster API:
@@ -90,7 +76,7 @@ This provider's versions are compatible with the following versions of Cluster A
 |                                   | Cluster API `v1beta1` (`v1.6.x`) | Cluster API `v1beta1` (`v1.7.x`) |
 | --------------------------------- | -------------------------------- | -------------------------------- |
 | Hetzner Provider `v1.0.0-beta.33` | ✅                              | ❌                               |
-| Hetzner Provider `v1.0.0-beta.34-35` | ❌                              | ✅                               |
+| Hetzner Provider `v1.0.0-beta.34-40` | ❌                              | ✅                               |
 
 This provider's versions can install and manage the following versions of Kubernetes:
 
@@ -114,45 +100,37 @@ Each version of Cluster API for Hetzner will attempt to support at least two Kub
 
 **NOTE:** As the versioning for this project is tied to the versioning of Cluster API, future modifications to this policy may be made to more closely align with other providers in the Cluster API ecosystem.
 
----
-
 ## 💿 Operating System Images
 
-> [!NOTE]
-> Cluster API Provider Hetzner relies on a few prerequisites that must be already installed in the operating system images, such as a container runtime, kubelet, and Kubeadm.
->
-> Reference images are available in kubernetes-sigs/image-builder and [templates/node-image](/templates/node-image).
->
-> If pre-installation of these prerequisites isn't possible, [custom scripts can be deployed](/docs/caph/02-topics/02-node-image) through the Kubeadm config.
+Cluster API Provider Hetzner relies on a few prerequisites that must be already installed in the operating system images, such as a container runtime, kubelet, and kubeadm.
 
----
+Reference images are available in kubernetes-sigs/image-builder and [templates/node-image](/templates/node-image).
 
-## 📖 Documentation
+If it's not possible to pre-install these prerequisites, [custom scripts can be deployed](/docs/caph/02-topics/02-node-image) through the kubeadm config.
 
-Documentation can be found at: [https://syself.com/docs/caph](https://syself.com/docs/caph)
+In case you want a solution with managed node images, [Syself](https://syself.com) might be interesting for you.
 
-## 👥 Getting Involved and Contributing
+## 🤝 Getting Involved and Contributing
 
-We, maintainers and the community, welcome any contributions to Cluster API Provider Hetzner. For suggestions, contributions, and assistance, contact the maintainers anytime.
+We, the maintainers and the community, welcome any contributions to Cluster API Provider Hetzner. Feel free to contact the maintainers for suggestions, contributions and help.
 
 To set up your environment, refer to the [development guide](https://syself.com/docs/caph/developers/development-guide).
 
 For new contributors, check out issues tagged as [`good first issue`][good_first_issue]. These are typically smaller in scope and great for getting familiar with the codebase.
 
-We encourage **all** active community participants to act as if they were maintainers, even without "official" write permissions. This is a community effort serving the Kubernetes community.
+We encourage **all** active community members to act as if they were maintainers, even without "official" write permissions. This is a collaborative effort serving the Kubernetes community.
 
 If you have an active interest and you want to get involved, you have real power! Don't assume that the only people who can get things done around here are the "maintainers".
 
-We would also love to add more "official" maintainers, so show us what you can
-do!
+We would also love to add more "official" maintainers, so show us what you can do!
 
-## ⚖️ Code of Conduct
+### ⚖️ Code of Conduct
 
 Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](/code-of-conduct.md).
 
-## :shipit: GitHub Issues
+### :shipit: GitHub Issues
 
-### 🐛 Bugs
+#### 🐛 Bugs
 
 If you think you have found a bug, please follow these steps:
 
@@ -162,7 +140,7 @@ If you think you have found a bug, please follow these steps:
 - Give it a meaningful title to help others who might be searching for your issue in the future.
 - For questions, reach out to the Cluster API community on the [Kubernetes Slack channel][slack_info].
 
-### 🌟 Tracking New Features
+#### 🌟 Tracking New Features
 
 We also use the issue tracker to track features. If you have an idea for a feature or think that you can help Cluster API Provider Hetzner become even more awesome, then follow these steps:
 
@@ -172,9 +150,14 @@ We also use the issue tracker to track features. If you have an idea for a featu
 - Some of our larger features will require some design. If you would like to include a technical design for your feature, please include it in the issue.
 - Once the new feature is well understood and the design is agreed upon, we can start coding. We would love for you to take part in this process, so we encourage you to take the lead and start coding it yourself. Please open a **WIP** _(work in progress)_ pull request. Happy coding!
 
-## 📃 License
+## 💬 Contact
+
+For more information about Syself, our platform, or any generall information about the Cluster API Provider Hetzner, feel free to reach out to us. Below are some ways to contact our team:
 
-Published under the [Apache](https://github.com/syself/cluster-api-provider-hetzner/blob/main/LICENSE) license.
+- **Email**: Send us questions at contact@syself.com
+- **Website**: Visit [our website](https://syself.com) for more information about Syself
+- **LinkedIn**: Follow us on [LinkedIn](https://www.linkedin.com/company/syself/) for announcements
+- **Newsletter**: Consider subscribing to [our LinkedIn newsletter](https://www.linkedin.com/newsletters/the-syselfer-7223788357485543424/) for regular news about CAPH
 
 <!-- References -->
 
diff --git a/api/v1beta1/hetznerbaremetalhost_types.go b/api/v1beta1/hetznerbaremetalhost_types.go
index 4b87f9d8..47be9682 100644
--- a/api/v1beta1/hetznerbaremetalhost_types.go
+++ b/api/v1beta1/hetznerbaremetalhost_types.go
@@ -152,9 +152,6 @@ const (
 	// StateImageInstalling means we install a new image.
 	StateImageInstalling ProvisioningState = "image-installing"
 
-	// StateProvisioning means we are sending userData to the host and booting the machine.
-	StateProvisioning ProvisioningState = "provisioning"
-
 	// StateEnsureProvisioned means we are ensuring the reboot worked and cloud-init was executed successfully.
 	StateEnsureProvisioned ProvisioningState = "ensure-provisioned"
 
diff --git a/api/v1beta1/hetznerbaremetalmachine_types.go b/api/v1beta1/hetznerbaremetalmachine_types.go
index 2952a96f..42b40760 100644
--- a/api/v1beta1/hetznerbaremetalmachine_types.go
+++ b/api/v1beta1/hetznerbaremetalmachine_types.go
@@ -149,7 +149,7 @@ type InstallImage struct {
 	// Image is the image to be provisioned. It defines the image for baremetal machine.
 	Image Image `json:"image"`
 
-	// PostInstallScript is used for configuring commands that should be executed after installimage.
+	// PostInstallScript (Bash) is used for configuring commands that should be executed after installimage.
 	// It is passed along with the installimage command.
 	PostInstallScript string `json:"postInstallScript,omitempty"`
 
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalhosts.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalhosts.yaml
index 45a71431..5317afa2 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalhosts.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalhosts.yaml
@@ -441,7 +441,7 @@ spec:
                         type: array
                       postInstallScript:
                         description: |-
-                          PostInstallScript is used for configuring commands that should be executed after installimage.
+                          PostInstallScript (Bash) is used for configuring commands that should be executed after installimage.
                           It is passed along with the installimage command.
                         type: string
                       swraid:
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachines.yaml
index 83402f08..c29e06e4 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachines.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachines.yaml
@@ -220,7 +220,7 @@ spec:
                     type: array
                   postInstallScript:
                     description: |-
-                      PostInstallScript is used for configuring commands that should be executed after installimage.
+                      PostInstallScript (Bash) is used for configuring commands that should be executed after installimage.
                       It is passed along with the installimage command.
                     type: string
                   swraid:
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachinetemplates.yaml
index c45afa75..9625e4c7 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachinetemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerbaremetalmachinetemplates.yaml
@@ -207,7 +207,7 @@ spec:
                             type: array
                           postInstallScript:
                             description: |-
-                              PostInstallScript is used for configuring commands that should be executed after installimage.
+                              PostInstallScript (Bash) is used for configuring commands that should be executed after installimage.
                               It is passed along with the installimage command.
                             type: string
                           swraid:
diff --git a/controllers/hetznerbaremetalhost_controller.go b/controllers/hetznerbaremetalhost_controller.go
index 92579bc7..0965bc4e 100644
--- a/controllers/hetznerbaremetalhost_controller.go
+++ b/controllers/hetznerbaremetalhost_controller.go
@@ -66,7 +66,6 @@ type HetznerBareMetalHostReconciler struct {
 // Reconcile implements the reconcilement of HetznerBareMetalHost objects.
 func (r *HetznerBareMetalHostReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, reterr error) {
 	log := ctrl.LoggerFrom(ctx)
-
 	// Fetch the Hetzner bare metal host instance.
 	bmHost := &infrav1.HetznerBareMetalHost{}
 	err := r.Get(ctx, req.NamespacedName, bmHost)
@@ -77,8 +76,6 @@ func (r *HetznerBareMetalHostReconciler) Reconcile(ctx context.Context, req ctrl
 		return reconcile.Result{}, err
 	}
 
-	log = log.WithValues("HetznerBareMetalHost", klog.KObj(bmHost))
-
 	// Add a finalizer to newly created objects.
 	if bmHost.DeletionTimestamp.IsZero() && !hostHasFinalizer(bmHost) {
 		bmHost.Finalizers = append(bmHost.Finalizers,
diff --git a/docs/README.md b/docs/README.md
index 6aa21df9..1128025c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -5,31 +5,32 @@ This is the official documentation of Cluster API Provider Hetzner. Before start
 ## Getting Started
 
 - [Introduction](/docs/caph/01-getting-started/01-introduction.md)
-- [Preparation](/docs/caph/01-getting-started/02-preparation)
-- [Quickstart](/docs/caph/01-getting-started/03-quickstart)
+- [Quickstart](/docs/caph/01-getting-started/02-quickstart)
+- [Preparation](/docs/caph/01-getting-started/03-preparation.md)
 
 ## Topics
 
-- [Managing SSH Keys](/docs/caph/02-topics/01-managing-ssh-keys)
-- [Node Images](/docs/caph/02-topics/02-node-image)
-- [Production Environment](/docs/caph/02-topics/03-production-environment)
-- [Advanced CAPH](/docs/caph/02-topics/04-advanced-caph)
-- [Upgrading CAPH](/docs/caph/02-topics/05-upgrading-caph)
-- [Hetzner Baremetal](/docs/caph/02-topics/06-hetzner-baremetal)
+- [Preparation](/docs/caph/02-topics/01-preparation.md)
+- [Managing SSH Keys](/docs/caph/02-topics/02-managing-ssh-keys.md)
+- [High Availability Setup](/docs/caph/02-topics/03-HA-setup.md)
+- [Node Image](/docs/caph/02-topics/04-node-image.md)
+- [Upgrading CAPH](/docs/caph/02-topics/05-upgrading-caph.md)
+- [Hetzner Baremetal](/docs/caph/02-topics/06-baremetal)
+- [Advanced CAPH](/docs/caph/02-topics/07-advanced)
 
 ## Reference
 
-- [General](/docs/caph/03-reference/01-introduction)
-- [HetznerCluster](/docs/caph/03-reference/02-hetzner-cluster)
-- [HCloudMachineTemplate](/docs/caph/03-reference/03-hcloud-machine-template)
-- [HCloudMachineTemplate](/docs/caph/03-reference/04-hcloud-remediation-template)
-- [HetznerBareMetalHost](/docs/caph/03-reference/05-hetzner-bare-metal-host)
-- [HetznerBareMetalMachineTemplate](/docs/caph/03-reference/06-hetzner-bare-metal-machine-template)
-- [HetznerBareMetalRemediationTemplate](/docs/caph/03-reference/07-hetzner-bare-metal-remediation-template)
+- [General](/docs/caph/03-reference/01-introduction.md)
+- [HetznerCluster](/docs/caph/03-reference/02-hetzner-cluster.md)
+- [HCloudMachineTemplate](/docs/caph/03-reference/03-hcloud-machine-template.md)
+- [HCloudMachineTemplate](/docs/caph/03-reference/04-hcloud-remediation-template.md)
+- [HetznerBareMetalHost](/docs/caph/03-reference/05-hetzner-bare-metal-host.md)
+- [HetznerBareMetalMachineTemplate](/docs/caph/03-reference/06-hetzner-bare-metal-machine-template.md)
+- [HetznerBareMetalRemediationTemplate](/docs/caph/03-reference/07-hetzner-bare-metal-remediation-template.md)
 
 ## Development
 
-- [Development guide](/docs/caph/04-developers/01-development-guide)
-- [Tilt](/docs/caph/04-developers/02-tilt)
-- [Releasing](/docs/caph/04-developers/03-releasing)
-- [Updating Kubernetes version](/docs/caph/04-developers/04-updating-kubernetes-version)
+- [Development guide](/docs/caph/04-developers/01-development-guide.md)
+- [Tilt](/docs/caph/04-developers/02-tilt.md)
+- [Releasing](/docs/caph/04-developers/03-releasing.md)
+- [Updating Kubernetes version](/docs/caph/04-developers/04-updating-kubernetes-version.md)
diff --git a/docs/caph/01-getting-started/02-quickstart/02-management-cluster-setup.md b/docs/caph/01-getting-started/02-quickstart/02-management-cluster-setup.md
index 49d323bd..c8cc6e02 100644
--- a/docs/caph/01-getting-started/02-quickstart/02-management-cluster-setup.md
+++ b/docs/caph/01-getting-started/02-quickstart/02-management-cluster-setup.md
@@ -19,7 +19,7 @@ There are several tasks that have to be completed before a workload cluster can
 
 1. Create a new [HCloud project](https://console.hetzner.cloud/projects).
 1. Generate an API token with read and write access. You'll find this if you click on the project and go to "security".
-1. If you want to use it, generate an SSH key, upload the public key to HCloud (also via "security"), and give it a name. Read more about [Managing SSH Keys](/docs/caph/02-topics/02-managing-ssh-keys).
+1. If you want to use it, generate an SSH key, upload the public key to HCloud (also via "security"), and give it a name. Read more about [Managing SSH Keys](/docs/caph/02-topics/02-managing-ssh-keys.md).
 
 ## Bootstrap or Management Cluster Installation
 
@@ -34,13 +34,13 @@ Cluster API requires an existing Kubernetes cluster accessible via kubectl. Duri
 
 It is a common practice to create a temporary, local bootstrap cluster, which is then used to provision a target management cluster on the selected infrastructure provider.
 
-## Choose one of the options below:
+## Choose one of the options below
 
-### 1. Existing Management Cluster.
+### 1. Existing Management Cluster
 
 For production use, a “real” Kubernetes cluster should be used with appropriate backup and DR policies and procedures in place. The Kubernetes cluster must be at least a [supported version](https://github.com/syself/cluster-api-provider-hetzner/blob/main/README.md#%EF%B8%8F-compatibility-with-cluster-api-and-kubernetes-versions).
 
-### 2. Kind.
+### 2. Kind
 
 [kind](https://kind.sigs.k8s.io/) can be used for creating a local Kubernetes cluster for development environments or for the creation of a temporary bootstrap cluster used to provision a target management cluster on the selected infrastructure provider.
 
@@ -74,7 +74,7 @@ For a specific version, use the `--infrastructure hetzner:vX.X.X` flag with the
 ## Variable Preparation to generate a cluster-template
 
 ```shell
-export HCLOUD_SSH_KEY="<ssh-key-name>" \
+export SSH_KEY_NAME="<ssh-key-name>" \
 export CLUSTER_NAME="my-cluster" \
 export HCLOUD_REGION="fsn1" \
 export CONTROL_PLANE_MACHINE_COUNT=3 \
@@ -84,7 +84,7 @@ export HCLOUD_CONTROL_PLANE_MACHINE_TYPE=cpx31 \
 export HCLOUD_WORKER_MACHINE_TYPE=cpx31
 ```
 
-- **HCLOUD_SSH_KEY**: The SSH Key name you loaded in HCloud.
+- **SSH_KEY_NAME**: The SSH Key name you loaded in HCloud.
 - **HCLOUD_REGION**: The region of the Hcloud cluster. Find the full list of regions [here](https://docs.hetzner.com/cloud/general/locations/).
 - **HCLOUD_IMAGE_NAME**: The Image name of the operating system.
 - **HCLOUD_X_MACHINE_TYPE**: The type of the Hetzner cloud server. Find more information [here](https://www.hetzner.com/cloud#pricing).
@@ -101,7 +101,7 @@ Running the above command will give you an output in the following manner:
 Required Variables:
   - HCLOUD_CONTROL_PLANE_MACHINE_TYPE
   - HCLOUD_REGION
-  - HCLOUD_SSH_KEY
+  - SSH_KEY_NAME
   - HCLOUD_WORKER_MACHINE_TYPE
 
 Optional Variables:
@@ -114,7 +114,7 @@ Optional Variables:
 
 In order for the provider integration hetzner to communicate with the Hetzner API ([HCloud API](https://docs.hetzner.cloud/)), we need to create a secret with the access data. The secret must be in the same namespace as the other CRs.
 
-`export HCLOUD_TOKEN="<YOUR-TOKEN>" `
+`export HCLOUD_TOKEN="<YOUR-TOKEN>"`
 
 - HCLOUD_TOKEN: The project where your cluster will be placed. You have to get a token from your HCloud Project.
 
diff --git a/docs/caph/01-getting-started/03-preparation.md b/docs/caph/01-getting-started/03-preparation.md
index f0e15b77..35fc46fa 100644
--- a/docs/caph/01-getting-started/03-preparation.md
+++ b/docs/caph/01-getting-started/03-preparation.md
@@ -10,7 +10,7 @@ There are several tasks that have to be completed before a workload cluster can
 
 1. Create a new [HCloud project](https://console.hetzner.cloud/projects).
 2. Generate an API token with read and write access. You'll find this if you click on the project and go to "security".
-3. If you want to use it, generate an SSH key, upload the public key to HCloud (also via "security"), and give it a name. Read more about [Managing SSH Keys](/docs/caph/02-topics/01-managing-ssh-keys).
+3. If you want to use it, generate an SSH key, upload the public key to HCloud (also via "security"), and give it a name. Read more about [Managing SSH Keys](/docs/caph/02-topics/02-managing-ssh-keys.md).
 
 ### Preparing Hetzner Robot
 
@@ -32,11 +32,11 @@ Cluster API requires an existing Kubernetes cluster accessible via kubectl. Duri
 
 It is a common practice to create a temporary, local bootstrap cluster, which is then used to provision a target management cluster on the selected infrastructure provider.
 
-### Choose one of the options below:
+### Choose one of the options below
 
 #### 1. Existing Management Cluster
 
-For production use, a “real” Kubernetes cluster should be used with appropriate backup and Disaster Recovery policies and procedures in place. The Kubernetes cluster must be of a [supported version](/docs/caph/01-getting-started/01-introduction).
+For production use, a “real” Kubernetes cluster should be used with appropriate backup and Disaster Recovery policies and procedures in place. The Kubernetes cluster must be of a [supported version](/docs/caph/01-getting-started/01-introduction.md).
 
 #### 2. Kind
 
@@ -65,10 +65,10 @@ Or for a specific [version](https://github.com/syself/cluster-api-provider-hetzn
 
 ---
 
-## Variable Preparation to generate a cluster-template.
+## Variable Preparation to generate a cluster-template
 
 ```shell
-export HCLOUD_SSH_KEY="<ssh-key-name>" \
+export SSH_KEY_NAME="<ssh-key-name>" \
 export CLUSTER_NAME="my-cluster" \
 export HCLOUD_REGION="fsn1" \
 export CONTROL_PLANE_MACHINE_COUNT=3 \
@@ -78,10 +78,10 @@ export HCLOUD_CONTROL_PLANE_MACHINE_TYPE=cpx31 \
 export HCLOUD_WORKER_MACHINE_TYPE=cpx31
 ```
 
-- HCLOUD_SSH_KEY: The SSH Key name you loaded in HCloud.
-- HCLOUD_REGION: https://docs.hetzner.com/cloud/general/locations/
+- SSH_KEY_NAME: The SSH Key name you loaded in HCloud.
+- HCLOUD_REGION: <https://docs.hetzner.com/cloud/general/locations/>
 - HCLOUD_IMAGE_NAME: The Image name of your operating system.
-- HCLOUD_X_MACHINE_TYPE: https://www.hetzner.com/cloud#pricing
+- HCLOUD_X_MACHINE_TYPE: <https://www.hetzner.com/cloud#pricing>
 
 For a list of all variables needed for generating a cluster manifest (from the cluster-template.yaml), use `clusterctl generate cluster --infrastructure hetzner:<caph-version> --list-variables hetzner-cluster`
 
@@ -90,7 +90,7 @@ $ clusterctl generate cluster --infrastructure hetzner:<caph-version> --list-var
 Required Variables:
   - HCLOUD_CONTROL_PLANE_MACHINE_TYPE
   - HCLOUD_REGION
-  - HCLOUD_SSH_KEY
+  - SSH_KEY_NAME
   - HCLOUD_WORKER_MACHINE_TYPE
 
 Optional Variables:
@@ -104,7 +104,7 @@ Optional Variables:
 
 In order for the provider integration hetzner to communicate with the Hetzner API ([HCloud API](https://docs.hetzner.cloud/)), we need to create a secret with the access data. The secret must be in the same namespace as the other CRs.
 
-`export HCLOUD_TOKEN="<YOUR-TOKEN>" `
+`export HCLOUD_TOKEN="<YOUR-TOKEN>"`
 
 - HCLOUD_TOKEN: The project where your cluster will be placed. You have to get a token from your HCloud Project.
 
@@ -147,4 +147,4 @@ kubectl patch secret robot-ssh -p '{"metadata":{"labels":{"clusterctl.cluster.x-
 
 The secret name and the tokens can also be customized in the cluster template.
 
-See [node-image](/docs/caph/02-topics/02-node-image) for more information.
+See [node-image](/docs/caph/02-topics/04-node-image.md) for more information.
diff --git a/docs/caph/02-topics/01-preparation.md b/docs/caph/02-topics/01-preparation.md
index c5626285..642e947c 100644
--- a/docs/caph/02-topics/01-preparation.md
+++ b/docs/caph/02-topics/01-preparation.md
@@ -8,7 +8,7 @@ There are several tasks that have to be completed before a workload cluster can
 
 1. Create a new [HCloud project](https://console.hetzner.cloud/projects).
 2. Generate an API token with read and write access. You'll find this if you click on the project and go to "security".
-3. If you want to use it, generate an SSH key, upload the public key to HCloud (also via "security"), and give it a name. Read more about [Managing SSH Keys](/docs/caph/02-topics/02-managing-ssh-keys).
+3. If you want to use it, generate an SSH key, upload the public key to HCloud (also via "security"), and give it a name. Read more about [Managing SSH Keys](/docs/caph/02-topics/02-managing-ssh-keys.md).
 
 ## Preparing Hetzner Robot
 
@@ -30,11 +30,11 @@ Cluster API requires an existing Kubernetes cluster accessible via kubectl. Duri
 
 It is a common practice to create a temporary, local bootstrap cluster, which is then used to provision a target management cluster on the selected infrastructure provider.
 
-### Choose one of the options below:
+### Choose one of the options below
 
 #### 1. Existing Management Cluster
 
-For production use, a “real” Kubernetes cluster should be used with appropriate backup and Disaster Recovery policies and procedures in place. The Kubernetes cluster must be of a [supported version](/docs/caph/01-getting-started/01-introduction).
+For production use, a “real” Kubernetes cluster should be used with appropriate backup and Disaster Recovery policies and procedures in place. The Kubernetes cluster must be of a [supported version](/docs/caph/01-getting-started/01-introduction.md).
 
 #### 2. Kind
 
@@ -63,10 +63,10 @@ Or for a specific [version](https://github.com/syself/cluster-api-provider-hetzn
 
 ---
 
-## Variable Preparation to generate a cluster-template.
+## Variable Preparation to generate a cluster-template
 
 ```shell
-export HCLOUD_SSH_KEY="<ssh-key-name>" \
+export SSH_KEY_NAME="<ssh-key-name>" \
 export CLUSTER_NAME="my-cluster" \
 export HCLOUD_REGION="fsn1" \
 export CONTROL_PLANE_MACHINE_COUNT=3 \
@@ -76,10 +76,10 @@ export HCLOUD_CONTROL_PLANE_MACHINE_TYPE=cpx31 \
 export HCLOUD_WORKER_MACHINE_TYPE=cpx31
 ```
 
-- HCLOUD_SSH_KEY: The SSH Key name you loaded in HCloud.
-- HCLOUD_REGION: https://docs.hetzner.com/cloud/general/locations/
+- SSH_KEY_NAME: The SSH Key name you loaded in HCloud.
+- HCLOUD_REGION: <https://docs.hetzner.com/cloud/general/locations/>
 - HCLOUD_IMAGE_NAME: The Image name of your operating system.
-- HCLOUD_X_MACHINE_TYPE: https://www.hetzner.com/cloud#pricing
+- HCLOUD_X_MACHINE_TYPE: <https://www.hetzner.com/cloud#pricing>
 
 For a list of all variables needed for generating a cluster manifest (from the cluster-template.yaml), use `clusterctl generate cluster --infrastructure hetzner:<caph-version> --list-variables hetzner-cluster`
 
@@ -88,7 +88,7 @@ $ clusterctl generate cluster --infrastructure hetzner:<caph-version> --list-var
 Required Variables:
   - HCLOUD_CONTROL_PLANE_MACHINE_TYPE
   - HCLOUD_REGION
-  - HCLOUD_SSH_KEY
+  - SSH_KEY_NAME
   - HCLOUD_WORKER_MACHINE_TYPE
 
 Optional Variables:
@@ -102,7 +102,7 @@ Optional Variables:
 
 In order for the provider integration hetzner to communicate with the Hetzner API ([HCloud API](https://docs.hetzner.cloud/)), we need to create a secret with the access data. The secret must be in the same namespace as the other CRs.
 
-`export HCLOUD_TOKEN="<YOUR-TOKEN>" `
+`export HCLOUD_TOKEN="<YOUR-TOKEN>"`
 
 - HCLOUD_TOKEN: The project where your cluster will be placed. You have to get a token from your HCloud Project.
 
@@ -145,4 +145,4 @@ kubectl patch secret robot-ssh -p '{"metadata":{"labels":{"clusterctl.cluster.x-
 
 The secret name and the tokens can also be customized in the cluster template.
 
-See [node-image](/docs/caph/02-topics/04-node-image) for more information.
+See [node-image](/docs/caph/02-topics/04-node-image.md) for more information.
diff --git a/docs/caph/02-topics/06-baremetal/01-introduction.md b/docs/caph/02-topics/06-baremetal/01-introduction.md
index f86c8e75..573361b0 100644
--- a/docs/caph/02-topics/06-baremetal/01-introduction.md
+++ b/docs/caph/02-topics/06-baremetal/01-introduction.md
@@ -31,4 +31,4 @@ These flavors are only for demonstration purposes and should not be used in prod
 
 ## Purchasing Bare Metal Servers
 
-If you want to create a cluster with bare metal servers, you will also need to set up the robot credentials. For setting robot credentials, as described in the [reference](/docs/caph/03-reference/06-hetzner-bare-metal-machine-template), you need to purchase bare metal servers beforehand manually.
+If you want to create a cluster with bare metal servers, you will also need to set up the robot credentials. For setting robot credentials, as described in the [reference](/docs/caph/03-reference/06-hetzner-bare-metal-machine-template.md), you need to purchase bare metal servers beforehand manually.
diff --git a/docs/caph/02-topics/06-baremetal/02-management-cluster.md b/docs/caph/02-topics/06-baremetal/02-management-cluster.md
index db2f1272..aba6ce47 100644
--- a/docs/caph/02-topics/06-baremetal/02-management-cluster.md
+++ b/docs/caph/02-topics/06-baremetal/02-management-cluster.md
@@ -35,7 +35,7 @@ $ clusterctl generate cluster my-cluster --list-variables --flavor hetzner-hclou
 Required Variables:
   - HCLOUD_CONTROL_PLANE_MACHINE_TYPE
   - HCLOUD_REGION
-  - HCLOUD_SSH_KEY
+  - SSH_KEY_NAME
   - HCLOUD_WORKER_MACHINE_TYPE
 
 Optional Variables:
@@ -94,7 +94,7 @@ ssh-keygen -t ed25519 -f ~/.ssh/caph
 
 Above command will create a public and private key in your `~/.ssh` directory.
 
-You can use the public key `~/.ssh/caph.pub` and upload it to your hcloud project. Go to your project and under `Security` -> `SSH Keys` click on `Add SSH key` and add your public key there and in the `Name` of ssh key you'll use the name `test`.
+You can use the public key `~/.ssh/caph.pub` and upload it to your hcloud project. Go to your project and under `Security` -> `SSH Keys` click on `Add SSH key` and add your public key there and in the `Name` of ssh key you'll use the name `my-caph-ssh-key`.
 
 {% callout %}
 
@@ -102,16 +102,16 @@ There is also a helper CLI called [hcloud](https://github.com/hetznercloud/cli)
 
 {% /callout %}
 
-In the above step, the name of the ssh-key that is recognized by hcloud is `test`. This is important because we will reference the name of the ssh-key later.
+In the above step, the name of the ssh-key that is recognized by hcloud is `my-caph-ssh-key`. This is important because we will reference the name of the ssh-key later.
 
 This is an important step because the same ssh key is used to access the servers. Make sure you are using the correct ssh key name.
 
-The `test` is the name of the ssh key that we have created above. It is because the generated manifest references `test` as the ssh key name.
+The `my-caph-ssh-key` is the name of the ssh key that we have created above. It is because the generated manifest references `my-caph-ssh-key` as the ssh key name.
 
 ```yaml
 sshKeys:
   hcloud:
-    - name: test
+    - name: my-caph-ssh-key
 ```
 
 {% callout %}
@@ -131,6 +131,7 @@ The `hetzner` secret contains API token for hcloud token. It also contains usern
 export HCLOUD_TOKEN="<YOUR-TOKEN>" \
 export HETZNER_ROBOT_USER="<YOUR-ROBOT-USER>" \
 export HETZNER_ROBOT_PASSWORD="<YOUR-ROBOT-PASSWORD>" \
+export SSH_KEY_NAME="<YOUR-SSH-KEY-NAME>" \
 export HETZNER_SSH_PUB_PATH="<YOUR-SSH-PUBLIC-PATH>" \
 export HETZNER_SSH_PRIV_PATH="<YOUR-SSH-PRIVATE-PATH>"
 ```
@@ -138,18 +139,21 @@ export HETZNER_SSH_PRIV_PATH="<YOUR-SSH-PRIVATE-PATH>"
 - `HCLOUD_TOKEN`: The project where your cluster will be placed. You have to get a token from your HCloud Project.
 - `HETZNER_ROBOT_USER`: The User you have defined in Robot under settings/web.
 - `HETZNER_ROBOT_PASSWORD`: The Robot Password you have set in Robot under settings/web.
+- `SSH_KEY_NAME`: The name of the SSH key you want to use.
 - `HETZNER_SSH_PUB_PATH`: The Path to your generated Public SSH Key.
 - `HETZNER_SSH_PRIV_PATH`: The Path to your generated Private SSH Key. This is needed because CAPH uses this key to provision the node in Hetzner Dedicated.
 
 ```shell
 kubectl create secret generic hetzner --from-literal=hcloud=$HCLOUD_TOKEN --from-literal=robot-user=$HETZNER_ROBOT_USER --from-literal=robot-password=$HETZNER_ROBOT_PASSWORD
 
-kubectl create secret generic robot-ssh --from-literal=sshkey-name=test --from-file=ssh-privatekey=$HETZNER_SSH_PRIV_PATH --from-file=ssh-publickey=$HETZNER_SSH_PUB_PATH
+kubectl create secret generic robot-ssh --from-literal=sshkey-name=$SSH_KEY_NAME \
+        --from-file=ssh-privatekey=$HETZNER_SSH_PRIV_PATH \
+        --from-file=ssh-publickey=$HETZNER_SSH_PUB_PATH
 ```
 
 {% callout %}
 
-`sshkey-name` should must match the name that is present in hetzner otherwise the controller will not know how to reach the machine.
+`sshkey-name` (from SSH_KEY_NAME) should must match the name that is present in Hetzner otherwise the controller will not know how to reach the machine. You can upload ssh-keys via the Robot UI (Server / Key Management).
 
 {% /callout %}
 
diff --git a/docs/caph/02-topics/07-advanced/04-custom-templates-mhc.md b/docs/caph/02-topics/07-advanced/04-custom-templates-mhc.md
index 659e32c8..488dad92 100644
--- a/docs/caph/02-topics/07-advanced/04-custom-templates-mhc.md
+++ b/docs/caph/02-topics/07-advanced/04-custom-templates-mhc.md
@@ -4,7 +4,7 @@ title: Machine Health Checks with Custom Remediation Template
 
 Cluster API allows to [configure Machine Health Checks](https://cluster-api.sigs.k8s.io/tasks/automated-machine-management/healthchecking.html) with custom remediation strategies. This is helpful for our bare metal servers. If the health checks give an outcome that one server cannot be reached, the default strategy would be to delete it. In that case, it would need to be provisioned again. This takes, of course, longer for bare metal servers than for virtual cloud servers. Therefore, we want to try to avoid this with the help of our `HetznerBareMetalRemediationController` and `HCloudRemediationController`. Instead of deleting the object and deprovisioning it, we first try to reboot it and see whether this helps. If it solves the problem, we save a lot of time that is required for re-provisioning it.
 
-If the MHC is configured to be used with the `HetznerBareMetalRemediationTemplate` (also see the [reference of the object](/docs/caph/03-reference/07-hetzner-bare-metal-remediation-template)) and `HCloudRemediationTemplate` (also see the [reference of the object](/docs/caph/03-reference/04-hcloud-remediation-template)), then such an object is created every time the MHC finds an unhealthy machine.
+If the MHC is configured to be used with the `HetznerBareMetalRemediationTemplate` (also see the [reference of the object](/docs/caph/03-reference/07-hetzner-bare-metal-remediation-template.md)) and `HCloudRemediationTemplate` (also see the [reference of the object](/docs/caph/03-reference/04-hcloud-remediation-template.md)), then such an object is created every time the MHC finds an unhealthy machine.
 
 The `HetznerBareMetalRemediationController` reconciles this object and then sets an annotation in the relevant `HetznerBareMetalHost` object specifying the desired remediation strategy. At the moment, only "reboot" is supported.
 The `HCloudRemediationController` reboots the HCloudMachine directly via the HCloud API. For HCloud servers, there is no other strategy than "reboot" either.
diff --git a/docs/caph/03-reference/02-hetzner-cluster.md b/docs/caph/03-reference/02-hetzner-cluster.md
index 81f62e0f..49a9aefb 100644
--- a/docs/caph/03-reference/02-hetzner-cluster.md
+++ b/docs/caph/03-reference/02-hetzner-cluster.md
@@ -16,7 +16,7 @@ In clusters with bare metal servers, you need to use [this CCM](https://github.c
 
 {% /callout %}
 
-[Here](/docs/caph/02-topics/02-managing-ssh-keys) you can find more information regarding the handling of SSH keys. Some of them are specified in `HetznerCluster` to have them cluster-wide, others are machine-scoped.
+[Here](/docs/caph/02-topics/02-managing-ssh-keys.md) you can find more information regarding the handling of SSH keys. Some of them are specified in `HetznerCluster` to have them cluster-wide, others are machine-scoped.
 
 ## Usage without HCloud Load Balancer
 
diff --git a/docs/caph/03-reference/03-hcloud-machine-template.md b/docs/caph/03-reference/03-hcloud-machine-template.md
index ff48a1cc..8ea57189 100644
--- a/docs/caph/03-reference/03-hcloud-machine-template.md
+++ b/docs/caph/03-reference/03-hcloud-machine-template.md
@@ -10,7 +10,7 @@ In `HCloudMachineTemplate` you can define all important properties for `HCloudMa
 | ------------------------------------------ | ---------- | --------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | `template.spec.providerID`                 | `string`   |                                         | no       | ProviderID set by controller                                                                                                                                                                                                                                                                    |
 | `template.spec.type`                       | `string`   |                                         | yes      | Desired server type of server in Hetzner's Cloud API. Example: cpx11                                                                                                                                                                                                                            |
-| `template.spec.imageName`                  | `string`   |                                         | yes      | Specifies desired image of server. ImageName can reference an image uploaded to Hetzner API in two ways: either directly as name of an image, or as label of an image (see [here](/docs/caph/02-topics/04-node-image) for more details) |
+| `template.spec.imageName`                  | `string`   |                                         | yes      | Specifies desired image of server. ImageName can reference an image uploaded to Hetzner API in two ways: either directly as name of an image, or as label of an image (see [here](/docs/caph/02-topics/04-node-image.md) for more details)                                                      |
 | `template.spec.sshKeys`                    | `object`   |                                         | no       | SSHKeys that are scoped to this machine                                                                                                                                                                                                                                                         |
 | `template.spec.sshKeys.hcloud`             | `[]object` |                                         | no       | SSH keys for HCloud                                                                                                                                                                                                                                                                             |
 | `template.spec.sshKeys.hcloud.name`        | `string`   |                                         | yes      | Name of SSH key                                                                                                                                                                                                                                                                                 |
diff --git a/docs/caph/03-reference/07-hetzner-bare-metal-remediation-template.md b/docs/caph/03-reference/07-hetzner-bare-metal-remediation-template.md
index 040fda21..5e5744b1 100644
--- a/docs/caph/03-reference/07-hetzner-bare-metal-remediation-template.md
+++ b/docs/caph/03-reference/07-hetzner-bare-metal-remediation-template.md
@@ -2,7 +2,7 @@
 title: HetznerBareMetalRemediationTemplate
 ---
 
-In `HetznerBareMetalRemediationTemplate` you can define all important properties for `HetznerBareMetalRemediations`. With this remediation, you can define a custom method for the manner of how Machine Health Checks treat the unhealthy `object`s - `HetznerBareMetalMachines` in this case. For more information about how to use remediations, see [Advanced CAPH](/docs/caph/02-topics/07-advanced/04-custom-templates-mhc). `HetznerBareMetalRemediations` are reconciled by the `HetznerBareMetalRemediationController`, which reconciles the remediatons and triggers the requested type of remediation on the relevant `HetznerBareMetalMachine`.
+In `HetznerBareMetalRemediationTemplate` you can define all important properties for `HetznerBareMetalRemediations`. With this remediation, you can define a custom method for the manner of how Machine Health Checks treat the unhealthy `object`s - `HetznerBareMetalMachines` in this case. For more information about how to use remediations, see [Advanced CAPH](/docs/caph/02-topics/07-advanced/04-custom-templates-mhc.md). `HetznerBareMetalRemediations` are reconciled by the `HetznerBareMetalRemediationController`, which reconciles the remediatons and triggers the requested type of remediation on the relevant `HetznerBareMetalMachine`.
 
 ## Overview of HetznerBareMetalRemediationTemplate.Spec
 
diff --git a/docs/caph/04-developers/01-development-guide.md b/docs/caph/04-developers/01-development-guide.md
index 5b0d6358..4cc1da8d 100644
--- a/docs/caph/04-developers/01-development-guide.md
+++ b/docs/caph/04-developers/01-development-guide.md
@@ -5,7 +5,7 @@ title: Developing Cluster API Provider Hetzner
 Developing our provider is quite easy. Please follow the steps mentioned below:
 
 1. You need to install some base requirements.
-2. You need to follow the [preparation document](/docs/caph/02-topics/01-preparation) to set up everything related to Hetzner.
+2. You need to follow the [preparation document](/docs/caph/02-topics/01-preparation.md) to set up everything related to Hetzner.
 
 ## Install Base requirements
 
@@ -26,13 +26,13 @@ This ensures the following:
 
 ## Preparing Hetzner project
 
-For more information, please see [here](/docs/caph/02-topics/01-preparation).
+For more information, please see [here](/docs/caph/02-topics/01-preparation.md).
 
 ## Setting Tilt up
 
 You need to create a `.envrc` file and specify the values you need. After the `.envrc` is loaded, invoke `direnv allow` to load the environment variables in your current shell session.
 
-The complete reference can be found [here](/docs/caph/04-developers/02-tilt).
+The complete reference can be found [here](/docs/caph/04-developers/02-tilt.md).
 
 ## Developing with Tilt
 
diff --git a/docs/caph/04-developers/04-updating-kubernetes-version.md b/docs/caph/04-developers/04-updating-kubernetes-version.md
index 19c6c3e2..e2594b98 100644
--- a/docs/caph/04-developers/04-updating-kubernetes-version.md
+++ b/docs/caph/04-developers/04-updating-kubernetes-version.md
@@ -7,4 +7,4 @@ Please check the kubernetes version in the following files:
 - `node-image` dependencies (cri-o, kubelet, kubeadm etc.).
 - `cluster-template` (same as node-image but for cloud-init).
 - quickstart Guide.
-- [Supported Versions](/docs/caph/01-getting-started/01-introduction).
+- [Supported Versions](/docs/caph/01-getting-started/01-introduction.md).
diff --git a/hack/filter-caph-controller-manager-logs.py b/hack/filter-caph-controller-manager-logs.py
index dfa936fa..4e618092 100755
--- a/hack/filter-caph-controller-manager-logs.py
+++ b/hack/filter-caph-controller-manager-logs.py
@@ -29,14 +29,24 @@ rows_to_skip = [
     'Registering a mutating webhook', 'Starting EventSource',
     'Starting Controller',
     '"Starting workers" controller/controller',
+    '"starting manager"',
+    '"Starting metrics server"',
+    '"starting server"',
     '"Reconciling finished"',
     '"Creating cluster scope"',
     '"Starting reconciling cluster"',
     '"Completed function"',
     '"Adding request."',
+    '"Serving metrics server"',
+    '"Starting webhook server"',
+    '"Serving webhook server"',
+    'attempting to acquire leader lease caph-system/hetzner.cluster.x-k8s.io.',
     'Update to resource only changes insignificant fields',
     '"approved csr"',
     '"Registering webhook"',
+    '"Stopping and waiting for',
+    '"All workers finished"',
+    '"Shutdown signal received, waiting for all workers to finish"',
     "'statusCode': 200, 'method': 'GET', 'url': 'https://robot-ws.your-server.de",
 ]
 
diff --git a/pkg/services/baremetal/baremetal/baremetal.go b/pkg/services/baremetal/baremetal/baremetal.go
index 8b835f8a..ba619fa5 100644
--- a/pkg/services/baremetal/baremetal/baremetal.go
+++ b/pkg/services/baremetal/baremetal/baremetal.go
@@ -504,7 +504,7 @@ func (s *Service) skipHost(labelSelector labels.Selector, host infrav1.HetznerBa
 		// Machine should have RAID. Skip machines which have less than two WWNs
 		lenOfWwnSlice := len(host.Spec.RootDeviceHints.Raid.WWN)
 		if lenOfWwnSlice < 2 {
-			mapOfSkipReasons[fmt.Sprintf("machine-should-use-swraid-but-only-%d-RAID-WWN-in-hbmh", lenOfWwnSlice)]++
+			mapOfSkipReasons["machine-should-use-swraid-but-not-enough-RAID-WWNs-in-hbmh"]++
 			return true
 		}
 		return false
diff --git a/pkg/services/baremetal/client/mocks/ssh/Client.go b/pkg/services/baremetal/client/mocks/ssh/Client.go
index db669999..f1590312 100644
--- a/pkg/services/baremetal/client/mocks/ssh/Client.go
+++ b/pkg/services/baremetal/client/mocks/ssh/Client.go
@@ -246,97 +246,6 @@ func (_c *Client_CreateAutoSetup_Call) RunAndReturn(run func(string) sshclient.O
 	return _c
 }
 
-// CreateMetaData provides a mock function with given fields: hostName
-func (_m *Client) CreateMetaData(hostName string) sshclient.Output {
-	ret := _m.Called(hostName)
-
-	if len(ret) == 0 {
-		panic("no return value specified for CreateMetaData")
-	}
-
-	var r0 sshclient.Output
-	if rf, ok := ret.Get(0).(func(string) sshclient.Output); ok {
-		r0 = rf(hostName)
-	} else {
-		r0 = ret.Get(0).(sshclient.Output)
-	}
-
-	return r0
-}
-
-// Client_CreateMetaData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateMetaData'
-type Client_CreateMetaData_Call struct {
-	*mock.Call
-}
-
-// CreateMetaData is a helper method to define mock.On call
-//   - hostName string
-func (_e *Client_Expecter) CreateMetaData(hostName interface{}) *Client_CreateMetaData_Call {
-	return &Client_CreateMetaData_Call{Call: _e.mock.On("CreateMetaData", hostName)}
-}
-
-func (_c *Client_CreateMetaData_Call) Run(run func(hostName string)) *Client_CreateMetaData_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(string))
-	})
-	return _c
-}
-
-func (_c *Client_CreateMetaData_Call) Return(_a0 sshclient.Output) *Client_CreateMetaData_Call {
-	_c.Call.Return(_a0)
-	return _c
-}
-
-func (_c *Client_CreateMetaData_Call) RunAndReturn(run func(string) sshclient.Output) *Client_CreateMetaData_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
-// CreateNoCloudDirectory provides a mock function with given fields:
-func (_m *Client) CreateNoCloudDirectory() sshclient.Output {
-	ret := _m.Called()
-
-	if len(ret) == 0 {
-		panic("no return value specified for CreateNoCloudDirectory")
-	}
-
-	var r0 sshclient.Output
-	if rf, ok := ret.Get(0).(func() sshclient.Output); ok {
-		r0 = rf()
-	} else {
-		r0 = ret.Get(0).(sshclient.Output)
-	}
-
-	return r0
-}
-
-// Client_CreateNoCloudDirectory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateNoCloudDirectory'
-type Client_CreateNoCloudDirectory_Call struct {
-	*mock.Call
-}
-
-// CreateNoCloudDirectory is a helper method to define mock.On call
-func (_e *Client_Expecter) CreateNoCloudDirectory() *Client_CreateNoCloudDirectory_Call {
-	return &Client_CreateNoCloudDirectory_Call{Call: _e.mock.On("CreateNoCloudDirectory")}
-}
-
-func (_c *Client_CreateNoCloudDirectory_Call) Run(run func()) *Client_CreateNoCloudDirectory_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run()
-	})
-	return _c
-}
-
-func (_c *Client_CreateNoCloudDirectory_Call) Return(_a0 sshclient.Output) *Client_CreateNoCloudDirectory_Call {
-	_c.Call.Return(_a0)
-	return _c
-}
-
-func (_c *Client_CreateNoCloudDirectory_Call) RunAndReturn(run func() sshclient.Output) *Client_CreateNoCloudDirectory_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
 // CreatePostInstallScript provides a mock function with given fields: data
 func (_m *Client) CreatePostInstallScript(data string) sshclient.Output {
 	ret := _m.Called(data)
@@ -383,52 +292,6 @@ func (_c *Client_CreatePostInstallScript_Call) RunAndReturn(run func(string) ssh
 	return _c
 }
 
-// CreateUserData provides a mock function with given fields: userData
-func (_m *Client) CreateUserData(userData string) sshclient.Output {
-	ret := _m.Called(userData)
-
-	if len(ret) == 0 {
-		panic("no return value specified for CreateUserData")
-	}
-
-	var r0 sshclient.Output
-	if rf, ok := ret.Get(0).(func(string) sshclient.Output); ok {
-		r0 = rf(userData)
-	} else {
-		r0 = ret.Get(0).(sshclient.Output)
-	}
-
-	return r0
-}
-
-// Client_CreateUserData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateUserData'
-type Client_CreateUserData_Call struct {
-	*mock.Call
-}
-
-// CreateUserData is a helper method to define mock.On call
-//   - userData string
-func (_e *Client_Expecter) CreateUserData(userData interface{}) *Client_CreateUserData_Call {
-	return &Client_CreateUserData_Call{Call: _e.mock.On("CreateUserData", userData)}
-}
-
-func (_c *Client_CreateUserData_Call) Run(run func(userData string)) *Client_CreateUserData_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(string))
-	})
-	return _c
-}
-
-func (_c *Client_CreateUserData_Call) Return(_a0 sshclient.Output) *Client_CreateUserData_Call {
-	_c.Call.Return(_a0)
-	return _c
-}
-
-func (_c *Client_CreateUserData_Call) RunAndReturn(run func(string) sshclient.Output) *Client_CreateUserData_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
 // DetectLinuxOnAnotherDisk provides a mock function with given fields: sliceOfWwns
 func (_m *Client) DetectLinuxOnAnotherDisk(sliceOfWwns []string) sshclient.Output {
 	ret := _m.Called(sliceOfWwns)
@@ -522,51 +385,6 @@ func (_c *Client_DownloadImage_Call) RunAndReturn(run func(string, string) sshcl
 	return _c
 }
 
-// EnsureCloudInit provides a mock function with given fields:
-func (_m *Client) EnsureCloudInit() sshclient.Output {
-	ret := _m.Called()
-
-	if len(ret) == 0 {
-		panic("no return value specified for EnsureCloudInit")
-	}
-
-	var r0 sshclient.Output
-	if rf, ok := ret.Get(0).(func() sshclient.Output); ok {
-		r0 = rf()
-	} else {
-		r0 = ret.Get(0).(sshclient.Output)
-	}
-
-	return r0
-}
-
-// Client_EnsureCloudInit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnsureCloudInit'
-type Client_EnsureCloudInit_Call struct {
-	*mock.Call
-}
-
-// EnsureCloudInit is a helper method to define mock.On call
-func (_e *Client_Expecter) EnsureCloudInit() *Client_EnsureCloudInit_Call {
-	return &Client_EnsureCloudInit_Call{Call: _e.mock.On("EnsureCloudInit")}
-}
-
-func (_c *Client_EnsureCloudInit_Call) Run(run func()) *Client_EnsureCloudInit_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run()
-	})
-	return _c
-}
-
-func (_c *Client_EnsureCloudInit_Call) Return(_a0 sshclient.Output) *Client_EnsureCloudInit_Call {
-	_c.Call.Return(_a0)
-	return _c
-}
-
-func (_c *Client_EnsureCloudInit_Call) RunAndReturn(run func() sshclient.Output) *Client_EnsureCloudInit_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
 // ExecuteInstallImage provides a mock function with given fields: hasPostInstallScript
 func (_m *Client) ExecuteInstallImage(hasPostInstallScript bool) sshclient.Output {
 	ret := _m.Called(hasPostInstallScript)
diff --git a/pkg/services/baremetal/client/ssh/ssh_client.go b/pkg/services/baremetal/client/ssh/ssh_client.go
index c9345e0a..6e76134c 100644
--- a/pkg/services/baremetal/client/ssh/ssh_client.go
+++ b/pkg/services/baremetal/client/ssh/ssh_client.go
@@ -176,6 +176,30 @@ type Output struct {
 	Err    error
 }
 
+func (o Output) String() string {
+	s := make([]string, 0, 3)
+	stdout := strings.TrimSpace(o.StdOut)
+	if stdout != "" {
+		s = append(s, stdout)
+	}
+	stderr := strings.TrimSpace(o.StdErr)
+	if stderr != "" {
+		if len(s) > 0 {
+			stderr = "Stderr: " + stderr
+		}
+		s = append(s, stderr)
+	}
+	if o.Err != nil {
+		e := o.Err.Error()
+		e = strings.TrimSpace(e)
+		if len(s) > 0 {
+			e = "Err: " + e
+		}
+		s = append(s, e)
+	}
+	return strings.Join(s, ". ")
+}
+
 // Client is the interface defining all functions necessary to talk to a bare metal server via SSH.
 type Client interface {
 	GetHostName() Output
@@ -196,10 +220,6 @@ type Client interface {
 	CreatePostInstallScript(data string) Output
 	ExecuteInstallImage(hasPostInstallScript bool) Output
 	Reboot() Output
-	EnsureCloudInit() Output
-	CreateNoCloudDirectory() Output
-	CreateMetaData(hostName string) Output
-	CreateUserData(userData string) Output
 	CloudInitStatus() Output
 	CheckCloudInitLogsForSigTerm() Output
 	CleanCloudInitLogs() Output
@@ -326,7 +346,7 @@ func (c *sshClient) GetHardwareDetailsCPUCores() Output {
 
 // GetHardwareDetailsDebug implements the GetHardwareDetailsDebug method of the SSHClient interface.
 func (c *sshClient) GetHardwareDetailsDebug() Output {
-	return c.runSSH(`ip a; echo ==========----------==========; 
+	return c.runSSH(`ip a; echo ==========----------==========;
 	ethtool "*"; echo ==========----------==========;
 	lspci; echo ==========----------==========;
 	`)
@@ -362,7 +382,7 @@ EOF_VIA_SSH`, data))
 	if out.Err != nil || out.StdErr != "" {
 		return out
 	}
-	return c.runSSH(`chmod +x /root/post-install.sh . `)
+	return c.runSSH(`chmod +x /root/post-install.sh`)
 }
 
 // GetRunningInstallImageProcesses returns the running installimage processes. Output.StdOut is empty if no processes are running.
@@ -432,30 +452,6 @@ func (c *sshClient) Reboot() Output {
 	return out
 }
 
-// EnsureCloudInit implements the EnsureCloudInit method of the SSHClient interface.
-func (c *sshClient) EnsureCloudInit() Output {
-	return c.runSSH(`command -v cloud-init`)
-}
-
-// CreateNoCloudDirectory implements the CreateNoCloudDirectory method of the SSHClient interface.
-func (c *sshClient) CreateNoCloudDirectory() Output {
-	return c.runSSH(`mkdir -p /var/lib/cloud/seed/nocloud-net`)
-}
-
-// CreateMetaData implements the CreateMetaData method of the SSHClient interface.
-func (c *sshClient) CreateMetaData(hostName string) Output {
-	return c.runSSH(fmt.Sprintf(`cat << 'EOF_VIA_SSH' > /var/lib/cloud/seed/nocloud-net/meta-data
-local-hostname: %s
-EOF_VIA_SSH`, hostName))
-}
-
-// CreateUserData implements the CreateUserData method of the SSHClient interface.
-func (c *sshClient) CreateUserData(userData string) Output {
-	return c.runSSH(fmt.Sprintf(`cat << 'EOF_VIA_SSH' > /var/lib/cloud/seed/nocloud-net/user-data
-%s
-EOF_VIA_SSH`, userData))
-}
-
 // CloudInitStatus implements the CloudInitStatus method of the SSHClient interface.
 func (c *sshClient) CloudInitStatus() Output {
 	out := c.runSSH("cloud-init status")
diff --git a/pkg/services/baremetal/host/host.go b/pkg/services/baremetal/host/host.go
index d7e9c9cc..9f096fd0 100644
--- a/pkg/services/baremetal/host/host.go
+++ b/pkg/services/baremetal/host/host.go
@@ -64,6 +64,10 @@ const (
 	errMsgInvalidSSHStdOut             = "invalid output in stdOut: %w"
 	errMsgFailedHandlingIncompleteBoot = "failed to handle incomplete boot: %w"
 	rebootServerStr                    = "RebootBMServer"
+
+	// PostInstallScriptFinished is a marker in the output of installimage. If it is not present,
+	// then install-image failed.
+	PostInstallScriptFinished = "POST_INSTALL_SCRIPT_FINISHED"
 )
 
 var (
@@ -130,7 +134,7 @@ func (s *Service) Reconcile(ctx context.Context) (result reconcile.Result, err e
 	}()
 
 	// reconcile state
-	actResult := hostStateMachine.ReconcileState()
+	actResult := hostStateMachine.ReconcileState(ctx)
 
 	result, err = actResult.Result()
 	if err != nil {
@@ -162,7 +166,9 @@ func SaveHostAndReturn(ctx context.Context, cl client.Client, host *infrav1.Hetz
 	return res, nil
 }
 
-func (s *Service) actionPreparing() actionResult {
+// previous: None
+// next: Registering
+func (s *Service) actionPreparing(_ context.Context) actionResult {
 	markProvisionPending(s.scope.HetznerBareMetalHost, infrav1.StatePreparing)
 
 	server, err := s.scope.RobotClient.GetBMServer(s.scope.HetznerBareMetalHost.Spec.ServerID)
@@ -238,14 +244,14 @@ func (s *Service) actionPreparing() actionResult {
 	if trimLineBreak(out.StdOut) != "" {
 		// we managed access with ssh - we can do an ssh reboot
 		if err := handleSSHError(sshClient.Reboot()); err != nil {
-			return actionError{err: fmt.Errorf("failed to reboot server via ssh: %w", err)}
+			return actionError{err: fmt.Errorf("failed to reboot server via ssh (actionPreparing): %w", err)}
 		}
 		msg := "Rebooting into rescue mode."
 		createSSHRebootEvent(s.scope.HetznerBareMetalHost, msg)
 		// we immediately set an error message in the host status to track the reboot we just performed
 		s.scope.HetznerBareMetalHost.SetError(infrav1.ErrorTypeSSHRebootTriggered, fmt.Sprintf("Phase %s, reboot via ssh: %s",
 			s.scope.HetznerBareMetalHost.Spec.Status.ProvisioningState, msg))
-		return actionComplete{}
+		return actionComplete{} // next: Registering
 	}
 
 	// Check if software reboot is available. If it is not, choose hardware reboot.
@@ -259,7 +265,7 @@ func (s *Service) actionPreparing() actionResult {
 	msg := createRebootEvent(s.scope.HetznerBareMetalHost, rebootType, "Reboot into rescue system.")
 	// we immediately set an error message in the host status to track the reboot we just performed
 	s.scope.HetznerBareMetalHost.SetError(errorType, msg)
-	return actionComplete{}
+	return actionComplete{} // next: Registering
 }
 
 func (s *Service) enforceRescueMode() error {
@@ -564,7 +570,9 @@ func (s *Service) ensureRescueMode() error {
 	return nil
 }
 
-func (s *Service) actionRegistering() actionResult {
+// previous: Preparing
+// next: ImageInstalling
+func (s *Service) actionRegistering(_ context.Context) actionResult {
 	markProvisionPending(s.scope.HetznerBareMetalHost, infrav1.StateRegistering)
 
 	creds := sshclient.CredentialsFromSecret(s.scope.RescueSSHSecret, s.scope.HetznerCluster.Spec.SSHKeys.RobotRescueSecretRef)
@@ -687,7 +695,7 @@ func (s *Service) actionRegistering() actionResult {
 
 	conditions.MarkTrue(s.scope.HetznerBareMetalHost, infrav1.RootDeviceHintsValidatedCondition)
 	s.scope.HetznerBareMetalHost.ClearError()
-	return actionComplete{}
+	return actionComplete{} // next: ImageInstalling
 }
 
 func validateRootDeviceWwnsAreSubsetOfExistingWwns(rootDeviceHints *infrav1.RootDeviceHints, storageDevices []infrav1.Storage) error {
@@ -722,8 +730,8 @@ func getHardwareDetails(sshClient sshclient.Client) (infrav1.HardwareDetails, er
 	}
 
 	// remove names of storage devices because they might change
-	for _, device := range storage {
-		device.Name = ""
+	for i := range storage {
+		storage[i].Name = ""
 	}
 
 	cpu, err := obtainHardwareDetailsCPU(sshClient)
@@ -1046,7 +1054,9 @@ func validateStdOut(stdOut string) (string, error) {
 	return stdOut, nil
 }
 
-func (s *Service) actionImageInstalling() actionResult {
+// previous: Registering
+// next: EnsureProvisioned
+func (s *Service) actionImageInstalling(ctx context.Context) actionResult {
 	markProvisionPending(s.scope.HetznerBareMetalHost, infrav1.StateImageInstalling)
 
 	creds := sshclient.CredentialsFromSecret(s.scope.RescueSSHSecret, s.scope.HetznerCluster.Spec.SSHKeys.RobotRescueSecretRef)
@@ -1112,11 +1122,10 @@ func (s *Service) actionImageInstalling() actionResult {
 		return actionError{err: fmt.Errorf("failed to get running installimage processes: %q %q %w", out.StdOut, out.StdErr, out.Err)}
 	}
 	if out.StdOut != "" {
-		record.Warnf(s.scope.HetznerBareMetalHost, "InstallImageAlreadyRunning",
-			"installimage is already running:\n%s", out.StdOut)
-		return actionContinue{
-			delay: 10 * time.Second,
-		}
+		// This can happen if the controller was restarted.
+		record.Warn(s.scope.HetznerBareMetalHost, "InstallImageAlreadyRunning",
+			"installimage is already running.")
+		return actionError{err: fmt.Errorf("installimage is already running")}
 	}
 
 	record.Event(s.scope.HetznerBareMetalHost, "InstallImagePreflightCheckSuccessful", "Rescue system reachable, disks look good.")
@@ -1140,10 +1149,38 @@ func (s *Service) actionImageInstalling() actionResult {
 	// create post install script
 	postInstallScript := s.scope.HetznerBareMetalHost.Spec.Status.InstallImage.PostInstallScript
 
-	if postInstallScript != "" {
-		if err := handleSSHError(sshClient.CreatePostInstallScript(postInstallScript)); err != nil {
-			return actionError{err: fmt.Errorf("failed to create post install script %s: %w", postInstallScript, err)}
-		}
+	if !strings.HasPrefix(postInstallScript, "#!/bin/bash") {
+		postInstallScript = fmt.Sprintf("#!/bin/bash\n%s", postInstallScript)
+	}
+
+	cloudInitData, err := s.scope.GetRawBootstrapData(ctx)
+	if err != nil {
+		return actionError{err: fmt.Errorf("failed to get user data: %w", err)}
+	}
+
+	postInstallScript = fmt.Sprintf(`%s
+
+# install cloud-init data
+
+trap 'echo "ERROR: A command has failed. Exiting the script. Line was ($0:$LINENO): $(sed -n "${LINENO}p" "$0")"; exit 3' ERR
+set -Eeuo pipefail
+
+mkdir -p /var/lib/cloud/seed/nocloud-net
+
+cat << 'EOF_POST_INSTALL_SCRIPT' > /var/lib/cloud/seed/nocloud-net/meta-data
+local-hostname: %s
+EOF_POST_INSTALL_SCRIPT
+
+cat << 'EOF_POST_INSTALL_SCRIPT' > /var/lib/cloud/seed/nocloud-net/user-data
+%s
+EOF_POST_INSTALL_SCRIPT
+
+echo %q
+# end of install cloud-init data
+`, postInstallScript, s.scope.Hostname(), cloudInitData, PostInstallScriptFinished)
+
+	if err := handleSSHError(sshClient.CreatePostInstallScript(postInstallScript)); err != nil {
+		return actionError{err: fmt.Errorf("failed to create post install script %s: %w", postInstallScript, err)}
 	}
 
 	record.Event(s.scope.HetznerBareMetalHost, "InstallingMachineImageStarted",
@@ -1160,10 +1197,14 @@ func (s *Service) actionImageInstalling() actionResult {
 	// Execute install image
 	out = sshClient.ExecuteInstallImage(postInstallScript != "")
 	if out.Err != nil {
-		record.Warnf(s.scope.HetznerBareMetalHost, "ExecuteInstallImageFailed", out.StdOut)
+		record.Warnf(s.scope.HetznerBareMetalHost, "ExecuteInstallImageFailed", out.String())
 		return actionError{err: fmt.Errorf("failed to execute installimage: %w", out.Err)}
 	}
-
+	if !strings.Contains(out.StdOut, PostInstallScriptFinished) {
+		record.Warnf(s.scope.HetznerBareMetalHost, "ExecuteInstallImageDidNotFinish", out.StdOut)
+		return actionError{err: fmt.Errorf("did not find marker %q in stdout. Failed to execute installimage",
+			PostInstallScriptFinished)}
+	}
 	record.Eventf(s.scope.HetznerBareMetalHost, "ExecuteInstallImageSucceeded", out.StdOut)
 	s.scope.Logger.Info("ExecuteInstallImageSucceeded", "stdout", out.StdOut, "stderr", out.StdErr)
 
@@ -1174,17 +1215,19 @@ func (s *Service) actionImageInstalling() actionResult {
 		return actionError{err: fmt.Errorf("failed to update name of host in robot API: %w", err)}
 	}
 
-	if err := handleSSHError(sshClient.Reboot()); err != nil {
+	out = sshClient.Reboot()
+	if err := handleSSHError(out); err != nil {
+		err = fmt.Errorf("failed to reboot server (after install-image): %w", err)
 		record.Warn(s.scope.HetznerBareMetalHost, "RebootFailed", err.Error())
-		return actionError{err: fmt.Errorf("failed to reboot server: %w", err)}
+		return actionError{err: err}
 	}
-	createSSHRebootEvent(s.scope.HetznerBareMetalHost, "machine image was installed")
+	createSSHRebootEvent(s.scope.HetznerBareMetalHost, "machine image and cloud-init data got installed")
 
 	s.scope.Logger.Info("RebootAfterInstallimageSucceeded", "stdout", out.StdOut, "stderr", out.StdErr)
 
 	// clear potential errors - all done
 	s.scope.HetznerBareMetalHost.ClearError()
-	return actionComplete{}
+	return actionComplete{} // next: ensure-provisioned
 }
 
 func (s *Service) createAutoSetupInput(sshClient sshclient.Client) (autoSetupInput, actionResult) {
@@ -1255,129 +1298,6 @@ func getDeviceNames(wwn []string, storageDevices []infrav1.Storage) []string {
 	return deviceNames
 }
 
-func (s *Service) actionProvisioning() actionResult {
-	markProvisionPending(s.scope.HetznerBareMetalHost, infrav1.StateProvisioning)
-
-	host := s.scope.HetznerBareMetalHost
-
-	portAfterInstallImage := host.Spec.Status.SSHSpec.PortAfterInstallImage
-	privateKey := sshclient.CredentialsFromSecret(s.scope.OSSSHSecret, host.Spec.Status.SSHSpec.SecretRef).PrivateKey
-	sshClient := s.scope.SSHClientFactory.NewClient(sshclient.Input{
-		PrivateKey: privateKey,
-		Port:       portAfterInstallImage,
-		IP:         host.Spec.Status.GetIPAddress(),
-	})
-
-	// check hostname with sshClient
-	wantHostName := s.scope.Hostname()
-
-	out := sshClient.GetHostName()
-
-	if out.Err != nil {
-		msg := fmt.Sprintf("ssh to port %d failed: %s (%s): %v", portAfterInstallImage, out.StdOut, out.StdErr, out.Err)
-		conditions.MarkFalse(host,
-			infrav1.SSHAfterInstallImageSucceededCondition,
-			infrav1.SSHAfterInstallImageFailedReason,
-			clusterv1.ConditionSeverityWarning, msg)
-
-		if time.Now().After(conditions.GetLastTransitionTime(
-			s.scope.HetznerBareMetalHost,
-			infrav1.SSHAfterInstallImageSucceededCondition).Add(7 * time.Minute)) {
-			// We waited some minutes. Still no connection. There seems to something wrong.
-			record.Warn(host, infrav1.SSHAfterInstallImageFailedReason, msg)
-		}
-	} else {
-		conditions.MarkTrue(s.scope.HetznerBareMetalHost,
-			infrav1.SSHAfterInstallImageSucceededCondition)
-	}
-
-	if trimLineBreak(out.StdOut) != wantHostName {
-		// Give the reboot some time until it takes effect. Otherwise the ssh connection gets done too fast,
-		// and it will connect to the machine before it gets rebooted.
-		if s.hasJustRebooted() {
-			s.scope.Logger.Info("hasJustRebooted is true", "LastUpdated", host.Spec.Status.LastUpdated)
-			return actionContinue{delay: 2 * time.Second}
-		}
-
-		privateKeyRescue := sshclient.CredentialsFromSecret(s.scope.RescueSSHSecret, s.scope.HetznerCluster.Spec.SSHKeys.RobotRescueSecretRef).PrivateKey
-		rescueSSHClient := s.scope.SSHClientFactory.NewClient(sshclient.Input{
-			PrivateKey: privateKeyRescue,
-			Port:       rescuePort,
-			IP:         host.Spec.Status.GetIPAddress(),
-		})
-
-		isSSHTimeoutError, isSSHConnectionRefusedError, err := analyzeSSHOutputInstallImage(out, rescueSSHClient, portAfterInstallImage)
-		if err != nil {
-			if errors.Is(err, errUnexpectedHostName) {
-				// One possible reason: The machine gets used by a second wl-cluster
-				record.Warn(host, "UnexpectedHostName",
-					fmt.Sprintf("Provisioning: wanted %q. %s", wantHostName, err.Error()))
-			}
-			return actionError{err: fmt.Errorf("failed to handle incomplete boot - installImage: %w", err)}
-		}
-		failed, err := s.handleIncompleteBoot(false, isSSHTimeoutError, isSSHConnectionRefusedError)
-		if failed {
-			return s.recordActionFailure(infrav1.ProvisioningError, err.Error())
-		}
-		if err != nil {
-			return actionError{err: fmt.Errorf(errMsgFailedHandlingIncompleteBoot, err)}
-		}
-		return actionContinue{delay: 10 * time.Second}
-	}
-
-	// we are in correct boot and can start provisioning
-	if failedAction := s.provision(sshClient); failedAction != nil {
-		return failedAction
-	}
-
-	host.ClearError()
-	return actionComplete{}
-}
-
-func (s *Service) provision(sshClient sshclient.Client) actionResult {
-	{
-		out := sshClient.EnsureCloudInit()
-		if err := handleSSHError(out); err != nil {
-			return actionError{err: fmt.Errorf("failed to ensure cloud init: %w", err)}
-		}
-
-		if trimLineBreak(out.StdOut) == "" {
-			msg := "cloud init not installed"
-			conditions.MarkFalse(
-				s.scope.HetznerBareMetalHost,
-				infrav1.ProvisionSucceededCondition,
-				infrav1.CloudInitNotInstalledReason,
-				clusterv1.ConditionSeverityError,
-				msg,
-			)
-			return s.recordActionFailure(infrav1.ProvisioningError, msg)
-		}
-	}
-
-	if err := handleSSHError(sshClient.CreateNoCloudDirectory()); err != nil {
-		return actionError{err: fmt.Errorf("failed to create no cloud directory: %w", err)}
-	}
-
-	if err := handleSSHError(sshClient.CreateMetaData(s.scope.Hostname())); err != nil {
-		return actionError{err: fmt.Errorf("failed to create meta data: %w", err)}
-	}
-
-	userData, err := s.scope.GetRawBootstrapData(context.TODO())
-	if err != nil {
-		return actionError{err: fmt.Errorf("failed to get user data: %w", err)}
-	}
-
-	if err := handleSSHError(sshClient.CreateUserData(string(userData))); err != nil {
-		return actionError{err: fmt.Errorf("failed to create user data: %w", err)}
-	}
-
-	if err := handleSSHError(sshClient.Reboot()); err != nil {
-		return actionError{err: fmt.Errorf("failed to reboot: %w", err)}
-	}
-	createSSHRebootEvent(s.scope.HetznerBareMetalHost, "UserData of cloud-init was created")
-	return nil
-}
-
 func analyzeSSHOutputInstallImage(out sshclient.Output, sshClient sshclient.Client, port int) (isTimeout, isConnectionRefused bool, reterr error) {
 	// check err
 	if out.Err != nil {
@@ -1441,7 +1361,7 @@ func verifyConnectionRefused(sshClient sshclient.Client, port int) bool {
 	return true
 }
 
-func (s *Service) actionEnsureProvisioned() (ar actionResult) {
+func (s *Service) actionEnsureProvisioned(_ context.Context) (ar actionResult) {
 	markProvisionPending(s.scope.HetznerBareMetalHost, infrav1.StateEnsureProvisioned)
 	sshClient := s.scope.SSHClientFactory.NewClient(sshclient.Input{
 		PrivateKey: sshclient.CredentialsFromSecret(s.scope.OSSSHSecret, s.scope.HetznerBareMetalHost.Spec.Status.SSHSpec.SecretRef).PrivateKey,
@@ -1482,9 +1402,11 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
 	wantHostName := s.scope.Hostname()
 
 	out := sshClient.GetHostName()
-	if trimLineBreak(out.StdOut) != wantHostName {
+	hostname := trimLineBreak(out.StdOut)
+	if hostname != wantHostName {
 		// give the reboot some time until it takes effect
 		if s.hasJustRebooted() {
+			s.scope.Logger.Info("ensureProvisioned: hasJustRebooted. Retrying...", "hostname", hostname)
 			return actionContinue{delay: 2 * time.Second}
 		}
 
@@ -1500,6 +1422,7 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
 		// A connection failed error could mean that cloud init is still running (if cloudInit introduces a new port)
 		if isSSHConnectionRefusedError {
 			if actionRes := s.handleConnectionRefused(); actionRes != nil {
+				s.scope.Logger.Info("ensureProvisioned: ConnectionRefused", "actionResult", actionRes)
 				return actionRes
 			}
 		}
@@ -1515,8 +1438,9 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
 	}
 
 	// Check the status of cloud init
-	actResult, _ := s.checkCloudInitStatus(sshClient)
+	actResult, msg, _ := s.checkCloudInitStatus(sshClient)
 	if _, complete := actResult.(actionComplete); !complete {
+		record.Event(s.scope.HetznerBareMetalHost, "CloudInitStillRunning", msg)
 		return actResult
 	}
 
@@ -1526,6 +1450,7 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
 	if s.scope.HetznerBareMetalHost.Spec.Status.SSHSpec.PortAfterInstallImage == s.scope.HetznerBareMetalHost.Spec.Status.SSHSpec.PortAfterCloudInit {
 		actResult = s.handleCloudInitNotStarted()
 		if _, complete := actResult.(actionComplete); !complete {
+			s.scope.Logger.Info("ensureProvisioned: handleCloudInitNotStarted", "actResult", actResult)
 			return actResult
 		}
 	}
@@ -1533,7 +1458,7 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
 	record.Event(s.scope.HetznerBareMetalHost, "ServerProvisioned", "server successfully provisioned")
 	conditions.MarkTrue(s.scope.HetznerBareMetalHost, infrav1.ProvisionSucceededCondition)
 	s.scope.HetznerBareMetalHost.ClearError()
-	return actionComplete{}
+	return actionComplete{} // next: EnsureProvisioned
 }
 
 // handleConnectionRefused checks cloud init status via ssh to the old ssh port if the new ssh port
@@ -1548,7 +1473,7 @@ func (s *Service) handleConnectionRefused() actionResult {
 		Port:       s.scope.HetznerBareMetalHost.Spec.Status.SSHSpec.PortAfterInstallImage,
 		IP:         s.scope.HetznerBareMetalHost.Spec.Status.GetIPAddress(),
 	})
-	actResult, err := s.checkCloudInitStatus(oldSSHClient)
+	actResult, _, err := s.checkCloudInitStatus(oldSSHClient)
 	// If this ssh client also gives an error, then we go back to analyzing the error of the first ssh call
 	// This happens in the statement below this one.
 	if err == nil {
@@ -1569,40 +1494,42 @@ func (s *Service) handleConnectionRefused() actionResult {
 	return nil
 }
 
-func (s *Service) checkCloudInitStatus(sshClient sshclient.Client) (actionResult, error) {
+func (s *Service) checkCloudInitStatus(sshClient sshclient.Client) (actionResult, string, error) {
 	out := sshClient.CloudInitStatus()
 	// This error is interesting for further logic and might happen because of the fact that the sshClient has the wrong port
 	if out.Err != nil {
-		return actionError{err: fmt.Errorf("failed to get cloud init status: %w", out.Err)}, out.Err
+		return actionError{err: fmt.Errorf("failed to get cloud init status: %w", out.Err)}, "", out.Err
 	}
 
 	stdOut := trimLineBreak(out.StdOut)
 	switch {
 	case strings.Contains(stdOut, "status: running"):
 		// Cloud init is still running
-		return actionContinue{delay: 5 * time.Second}, nil
+		return actionContinue{delay: 5 * time.Second}, "cloud-init still running", nil
 	case strings.Contains(stdOut, "status: disabled"):
 		// Reboot needs to be triggered again - did not start yet
 		out = sshClient.Reboot()
+		msg := "cloud-init-status was 'disabled'"
 		if err := handleSSHError(out); err != nil {
-			return actionError{err: fmt.Errorf("failed to reboot: %w", err)}, nil
+			return actionError{err: fmt.Errorf("failed to reboot (%s): %w", msg, err)}, "", nil
 		}
+		createSSHRebootEvent(s.scope.HetznerBareMetalHost, msg)
 		s.scope.HetznerBareMetalHost.SetError(infrav1.ErrorTypeSSHRebootTriggered, "ssh reboot just triggered")
-		record.Event(s.scope.HetznerBareMetalHost, "SSHRebootAfterCloudInitStatusDisabled", "cloud-init status was 'disabled'")
-		return actionContinue{delay: 5 * time.Second}, nil
+		record.Warn(s.scope.HetznerBareMetalHost, "SSHRebootAfterCloudInitStatusDisabled", msg)
+		return actionContinue{delay: 5 * time.Second}, "cloud-init was disabled. Triggered a reboot again", nil
 	case strings.Contains(stdOut, "status: done"):
-
 		s.scope.HetznerBareMetalHost.ClearError()
+		return actionComplete{}, "cloud-init is done", nil
 	case strings.Contains(stdOut, "status: error"):
 		record.Warn(s.scope.HetznerBareMetalHost, "CloudInitFailed", "cloud init returned status error")
-		return s.recordActionFailure(infrav1.FatalError, "cloud init returned status error"), nil
+		return s.recordActionFailure(infrav1.FatalError, "cloud init returned status error"), "", nil
 	default:
 		// Errors are handled after stdOut in this case, as status: error returns an exited with status 1 error
 		if err := handleSSHError(out); err != nil {
-			return actionError{err: fmt.Errorf("failed to get cloud init status: %w", err)}, nil
+			return actionError{err: fmt.Errorf("failed to get cloud init status: %w", err)}, "", nil
 		}
+		return actionContinue{delay: 5 * time.Second}, fmt.Sprintf("cloud-init unknown output: %s. %s", out.StdOut, out.StdErr), nil
 	}
-	return actionComplete{}, nil
 }
 
 func (s *Service) handleCloudInitNotStarted() actionResult {
@@ -1629,8 +1556,9 @@ func (s *Service) handleCloudInitNotStarted() actionResult {
 		}
 		out = oldSSHClient.Reboot()
 		if err := handleSSHError(out); err != nil {
-			return actionError{err: fmt.Errorf("failed to reboot: %w", err)}
+			return actionError{err: fmt.Errorf("failed to reboot (handleCloudInitNotStarted): %w", err)}
 		}
+		createSSHRebootEvent(s.scope.HetznerBareMetalHost, "machine image and cloud-init data got installed")
 		record.Eventf(s.scope.HetznerBareMetalHost,
 			"SSHRebootAfterCloudInitSigTermFound", "rebooted via ssh after cloud init logs contained sigterm: %s", trimLineBreak(out.StdOut))
 		return actionContinue{delay: 10 * time.Second}
@@ -1675,7 +1603,9 @@ func analyzeSSHOutputProvisioned(out sshclient.Output) (isTimeout, isConnectionR
 	return false, false, fmt.Errorf("%w: %s", errUnexpectedHostName, trimLineBreak(out.StdOut))
 }
 
-func (s *Service) actionProvisioned() actionResult {
+// previous: EnsureProvisioned
+// next: Stays in Provisioned (final state)
+func (s *Service) actionProvisioned(_ context.Context) actionResult {
 	// set host to provisioned
 	conditions.MarkTrue(s.scope.HetznerBareMetalHost, infrav1.ProvisionSucceededCondition)
 
@@ -1730,15 +1660,16 @@ func (s *Service) actionProvisioned() actionResult {
 			return actionError{err: err}
 		}
 
-		createSSHRebootEvent(s.scope.HetznerBareMetalHost, "reboot annotation was set")
+		createSSHRebootEvent(s.scope.HetznerBareMetalHost, "Rebooting because annotation was set")
 		s.scope.HetznerBareMetalHost.Spec.Status.Rebooted = true
 		return actionContinue{delay: 10 * time.Second}
 	}
 
-	return actionComplete{}
+	return actionComplete{} // Stays in Provisioned (final state)
 }
 
-func (s *Service) actionDeprovisioning() actionResult {
+// next: None
+func (s *Service) actionDeprovisioning(_ context.Context) actionResult {
 	// Update name in robot API
 	if _, err := s.scope.RobotClient.SetBMServerName(
 		s.scope.HetznerBareMetalHost.Spec.ServerID,
@@ -1772,11 +1703,10 @@ func (s *Service) actionDeprovisioning() actionResult {
 		s.scope.HetznerBareMetalHost.ClearError()
 		conditions.Delete(s.scope.HetznerBareMetalHost, infrav1.ProvisionSucceededCondition)
 	}
-
-	return actionComplete{}
+	return actionComplete{} // next: None
 }
 
-func (s *Service) actionDeleting() actionResult {
+func (s *Service) actionDeleting(_ context.Context) actionResult {
 	s.scope.HetznerBareMetalHost.Finalizers = utils.FilterStringFromList(s.scope.HetznerBareMetalHost.Finalizers, infrav1.BareMetalHostFinalizer)
 	return deleteComplete{}
 }
diff --git a/pkg/services/baremetal/host/state_machine.go b/pkg/services/baremetal/host/state_machine.go
index a6086af0..cc336a16 100644
--- a/pkg/services/baremetal/host/state_machine.go
+++ b/pkg/services/baremetal/host/state_machine.go
@@ -17,6 +17,7 @@ limitations under the License.
 package host
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/go-logr/logr"
@@ -51,14 +52,13 @@ func newHostStateMachine(host *infrav1.HetznerBareMetalHost, reconciler *Service
 	return &r
 }
 
-type stateHandler func() actionResult
+type stateHandler func(ctx context.Context) actionResult
 
 func (hsm *hostStateMachine) handlers() map[infrav1.ProvisioningState]stateHandler {
 	return map[infrav1.ProvisioningState]stateHandler{
 		infrav1.StatePreparing:         hsm.handlePreparing,
 		infrav1.StateRegistering:       hsm.handleRegistering,
 		infrav1.StateImageInstalling:   hsm.handleImageInstalling,
-		infrav1.StateProvisioning:      hsm.handleProvisioning,
 		infrav1.StateEnsureProvisioned: hsm.handleEnsureProvisioned,
 		infrav1.StateProvisioned:       hsm.handleProvisioned,
 		infrav1.StateDeprovisioning:    hsm.handleDeprovisioning,
@@ -66,7 +66,7 @@ func (hsm *hostStateMachine) handlers() map[infrav1.ProvisioningState]stateHandl
 	}
 }
 
-func (hsm *hostStateMachine) ReconcileState() (actionRes actionResult) {
+func (hsm *hostStateMachine) ReconcileState(ctx context.Context) (actionRes actionResult) {
 	initialState := hsm.host.Spec.Status.ProvisioningState
 	defer func() {
 		if hsm.nextState != initialState {
@@ -87,8 +87,17 @@ func (hsm *hostStateMachine) ReconcileState() (actionRes actionResult) {
 	// Assume credentials are ready for now. This can be changed while the state is handled.
 	conditions.MarkTrue(hsm.host, infrav1.CredentialsAvailableCondition)
 
+	// This state was removed. We have to handle the edge-case where
+	// the controller got updated and a machine
+	// is in this state. Installing the image again should solve that.
+	if initialState == "provisioning" {
+		hsm.log.Info("edge-case was hit: New code meets machine in removed state 'provisioning'. Re-setting",
+			"new-state", infrav1.StateImageInstalling)
+		initialState = infrav1.StateImageInstalling
+	}
+
 	if stateHandler, found := hsm.handlers()[initialState]; found {
-		return stateHandler()
+		return stateHandler(ctx)
 	}
 
 	return actionError{fmt.Errorf("%w: state %q", errNoHandlerFound, initialState)}
@@ -103,7 +112,7 @@ func (hsm *hostStateMachine) checkInitiateDelete() bool {
 	switch hsm.nextState {
 	default:
 		hsm.nextState = infrav1.StateDeleting
-	case infrav1.StateRegistering, infrav1.StateImageInstalling, infrav1.StateProvisioning,
+	case infrav1.StateRegistering, infrav1.StateImageInstalling,
 		infrav1.StateEnsureProvisioned, infrav1.StateProvisioned:
 		hsm.nextState = infrav1.StateDeprovisioning
 	case infrav1.StateDeprovisioning:
@@ -149,7 +158,7 @@ func (hsm *hostStateMachine) updateOSSSHStatusAndValidateKey(osSSHSecret *corev1
 	if !hsm.host.Spec.Status.SSHStatus.CurrentOS.Match(*osSSHSecret) {
 		// Take action depending on state
 		switch hsm.nextState {
-		case infrav1.StateProvisioning, infrav1.StateEnsureProvisioned:
+		case infrav1.StateEnsureProvisioned:
 			// Go back to StateImageInstalling as we need to provision again
 			hsm.nextState = infrav1.StateImageInstalling
 		case infrav1.StateProvisioned:
@@ -214,7 +223,7 @@ func validateSSHKey(sshSecret *corev1.Secret, secretRef infrav1.SSHSecretRef) er
 	return nil
 }
 
-func (hsm *hostStateMachine) handlePreparing() actionResult {
+func (hsm *hostStateMachine) handlePreparing(ctx context.Context) actionResult {
 	if hsm.provisioningCancelled() {
 		hsm.nextState = infrav1.StateDeprovisioning
 		return actionComplete{}
@@ -222,75 +231,72 @@ func (hsm *hostStateMachine) handlePreparing() actionResult {
 
 	record.Eventf(hsm.host, "PreparingForProvisioning", "ServerID %d %s", hsm.host.Spec.ServerID, hsm.host.Spec.Description)
 
-	actResult := hsm.reconciler.actionPreparing()
+	actResult := hsm.reconciler.actionPreparing(ctx)
 	if _, ok := actResult.(actionComplete); ok {
 		hsm.nextState = infrav1.StateRegistering
 	}
 	return actResult
 }
 
-func (hsm *hostStateMachine) handleRegistering() actionResult {
+func (hsm *hostStateMachine) handleRegistering(ctx context.Context) actionResult {
 	if hsm.provisioningCancelled() {
 		hsm.nextState = infrav1.StateDeprovisioning
 		return actionComplete{}
 	}
 
-	actResult := hsm.reconciler.actionRegistering()
+	actResult := hsm.reconciler.actionRegistering(ctx)
 	if _, ok := actResult.(actionComplete); ok {
 		hsm.nextState = infrav1.StateImageInstalling
 	}
 	return actResult
 }
 
-func (hsm *hostStateMachine) handleImageInstalling() actionResult {
-	if hsm.provisioningCancelled() {
-		hsm.nextState = infrav1.StateDeprovisioning
-		return actionComplete{}
-	}
-
-	actResult := hsm.reconciler.actionImageInstalling()
-	if _, ok := actResult.(actionComplete); ok {
-		hsm.nextState = infrav1.StateProvisioning
-	}
-	return actResult
-}
-
-func (hsm *hostStateMachine) handleProvisioning() actionResult {
+func (hsm *hostStateMachine) handleImageInstalling(ctx context.Context) actionResult {
 	if hsm.provisioningCancelled() {
 		hsm.nextState = infrav1.StateDeprovisioning
 		return actionComplete{}
 	}
 
-	actResult := hsm.reconciler.actionProvisioning()
-	if _, ok := actResult.(actionComplete); ok {
+	actResult := hsm.reconciler.actionImageInstalling(ctx)
+	switch actResult.(type) {
+	case actionComplete:
 		hsm.nextState = infrav1.StateEnsureProvisioned
+	case actionError:
+		// re-enable rescue system. If installimage failed, then it is likely, that
+		// the next run (without reboot) fails with this error:
+		// ERROR unmounting device(s):
+		// umount: /: target is busy.
+		// Cannot continue, device(s) seem to be in use.
+		// Please unmount used devices manually or reboot the rescuesystem and retry.
+		hsm.nextState = infrav1.StatePreparing
 	}
+
 	return actResult
 }
 
-func (hsm *hostStateMachine) handleEnsureProvisioned() actionResult {
+func (hsm *hostStateMachine) handleEnsureProvisioned(ctx context.Context) actionResult {
 	if hsm.provisioningCancelled() {
 		hsm.nextState = infrav1.StateDeprovisioning
 		return actionComplete{}
 	}
 
-	actResult := hsm.reconciler.actionEnsureProvisioned()
+	actResult := hsm.reconciler.actionEnsureProvisioned(ctx)
 	if _, ok := actResult.(actionComplete); ok {
 		hsm.nextState = infrav1.StateProvisioned
 	}
 	return actResult
 }
 
-func (hsm *hostStateMachine) handleProvisioned() actionResult {
+func (hsm *hostStateMachine) handleProvisioned(ctx context.Context) actionResult {
 	if hsm.provisioningCancelled() {
 		hsm.nextState = infrav1.StateDeprovisioning
 		return actionComplete{}
 	}
-	return hsm.reconciler.actionProvisioned()
+	return hsm.reconciler.actionProvisioned(ctx)
 }
 
-func (hsm *hostStateMachine) handleDeprovisioning() actionResult {
-	actResult := hsm.reconciler.actionDeprovisioning()
+func (hsm *hostStateMachine) handleDeprovisioning(ctx context.Context) actionResult {
+	actResult := hsm.reconciler.actionDeprovisioning(ctx)
 	if _, ok := actResult.(actionComplete); ok {
 		hsm.nextState = infrav1.StateNone
 		return actionComplete{}
@@ -298,8 +304,8 @@ func (hsm *hostStateMachine) handleDeprovisioning() actionResult {
 	return actResult
 }
 
-func (hsm *hostStateMachine) handleDeleting() actionResult {
-	return hsm.reconciler.actionDeleting()
+func (hsm *hostStateMachine) handleDeleting(ctx context.Context) actionResult {
+	return hsm.reconciler.actionDeleting(ctx)
 }
 
 func (hsm *hostStateMachine) provisioningCancelled() bool {
diff --git a/templates/cluster-templates/bases/hcloud-hetznerCluster-network.yaml b/templates/cluster-templates/bases/hcloud-hetznerCluster-network.yaml
index dfaf2e4f..10237e4b 100644
--- a/templates/cluster-templates/bases/hcloud-hetznerCluster-network.yaml
+++ b/templates/cluster-templates/bases/hcloud-hetznerCluster-network.yaml
@@ -14,10 +14,10 @@ spec:
     region: "${HCLOUD_REGION}"
   sshKeys:
     hcloud:
-      - name: "${HCLOUD_SSH_KEY}"
+      - name: "${SSH_KEY_NAME}"
   hetznerSecretRef:
     name: hetzner
     key:
       hcloudToken: hcloud
       hetznerRobotPassword: robot-password
-      hetznerRobotUser: robot-user
\ No newline at end of file
+      hetznerRobotUser: robot-user
diff --git a/templates/cluster-templates/bases/hcloud-hetznerCluster.yaml b/templates/cluster-templates/bases/hcloud-hetznerCluster.yaml
index e1d75483..85999bae 100644
--- a/templates/cluster-templates/bases/hcloud-hetznerCluster.yaml
+++ b/templates/cluster-templates/bases/hcloud-hetznerCluster.yaml
@@ -14,7 +14,7 @@ spec:
     region: "${HCLOUD_REGION}"
   sshKeys:
     hcloud:
-      - name: "${HCLOUD_SSH_KEY}"
+      - name: "${SSH_KEY_NAME}"
   hetznerSecretRef:
     name: hetzner
     key:
diff --git a/templates/cluster-templates/bases/hetzner-hetznerCluster.yaml b/templates/cluster-templates/bases/hetzner-hetznerCluster.yaml
index 8bc76f60..40b6014c 100644
--- a/templates/cluster-templates/bases/hetzner-hetznerCluster.yaml
+++ b/templates/cluster-templates/bases/hetzner-hetznerCluster.yaml
@@ -14,7 +14,7 @@ spec:
     region: "${HCLOUD_REGION}"
   sshKeys:
     hcloud:
-      - name: "${HCLOUD_SSH_KEY}"
+      - name: "${SSH_KEY_NAME}"
     robotRescueSecretRef:
       name: robot-ssh
       key:


Test patch:
diff --git a/controllers/hcloudmachine_controller_test.go b/controllers/hcloudmachine_controller_test.go
index 6eed7543..37bef3a8 100644
--- a/controllers/hcloudmachine_controller_test.go
+++ b/controllers/hcloudmachine_controller_test.go
@@ -127,9 +127,11 @@ func TestIgnoreInsignificantHCloudMachineStatusUpdates(t *testing.T) {
 		})
 	}
 }
+
 func stPtr(h hcloud.ServerStatus) *hcloud.ServerStatus {
 	return &h
 }
+
 func TestIgnoreInsignificantMachineStatusUpdates(t *testing.T) {
 	logger := klog.Background()
 	predicate := IgnoreInsignificantMachineStatusUpdates(logger)
@@ -361,7 +363,7 @@ var _ = Describe("HCloudMachineReconciler", func() {
 				}, timeout).Should(BeTrue())
 			})
 
-			It("creates the HCloud machine in Hetzner", func() {
+			It("creates the HCloud machine in Hetzner 1", func() {
 				By("checking that no servers exist")
 
 				Eventually(func() bool {
@@ -518,7 +520,7 @@ var _ = Describe("HCloudMachineReconciler", func() {
 				Expect(testEnv.Cleanup(ctx, hetznerCluster, hcloudMachine)).To(Succeed())
 			})
 
-			It("creates the HCloud machine in Hetzner", func() {
+			It("creates the HCloud machine in Hetzner 2", func() {
 				Eventually(func() int {
 					servers, err := hcloudClient.ListServers(ctx, hcloud.ServerListOpts{
 						ListOpts: hcloud.ListOpts{
@@ -546,7 +548,7 @@ var _ = Describe("HCloudMachineReconciler", func() {
 				Expect(testEnv.Cleanup(ctx, hetznerCluster, hcloudMachine)).To(Succeed())
 			})
 
-			It("creates the HCloud machine in Hetzner", func() {
+			It("creates the HCloud machine in Hetzner 3", func() {
 				Eventually(func() int {
 					servers, err := hcloudClient.ListServers(ctx, hcloud.ServerListOpts{
 						ListOpts: hcloud.ListOpts{
@@ -594,7 +596,7 @@ var _ = Describe("HCloudMachineReconciler", func() {
 				Expect(testEnv.Cleanup(ctx, hetznerCluster, hcloudMachine)).To(Succeed())
 			})
 
-			It("creates the HCloud machine in Hetzner", func() {
+			It("creates the HCloud machine in Hetzner 4", func() {
 				Eventually(func() int {
 					servers, err := hcloudClient.ListServers(ctx, hcloud.ServerListOpts{
 						ListOpts: hcloud.ListOpts{
diff --git a/controllers/hetznerbaremetalhost_controller_test.go b/controllers/hetznerbaremetalhost_controller_test.go
index fc244c75..d8d83230 100644
--- a/controllers/hetznerbaremetalhost_controller_test.go
+++ b/controllers/hetznerbaremetalhost_controller_test.go
@@ -36,6 +36,7 @@ import (
 	robotmock "github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/client/mocks/robot"
 	sshmock "github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/client/mocks/ssh"
 	sshclient "github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/client/ssh"
+	hostpkg "github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/host"
 	"github.com/syself/cluster-api-provider-hetzner/pkg/utils"
 	"github.com/syself/cluster-api-provider-hetzner/test/helpers"
 )
@@ -162,10 +163,6 @@ var _ = Describe("HetznerBareMetalHostReconciler", func() {
 		configureRescueSSHClient(rescueSSHClient)
 
 		osSSHClientAfterInstallImage.On("Reboot").Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("CreateNoCloudDirectory").Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("CreateMetaData", mock.Anything).Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("CreateUserData", mock.Anything).Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("EnsureCloudInit").Return(sshclient.Output{StdOut: "cloud-init"})
 		osSSHClientAfterInstallImage.On("CloudInitStatus").Return(sshclient.Output{StdOut: "status: done"})
 		osSSHClientAfterInstallImage.On("CheckCloudInitLogsForSigTerm").Return(sshclient.Output{})
 		osSSHClientAfterInstallImage.On("ResetKubeadm").Return(sshclient.Output{})
@@ -895,7 +892,7 @@ name="eth0" model="Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express
 	sshClient.On("CreateAutoSetup", mock.Anything).Return(sshclient.Output{})
 	sshClient.On("UntarTGZ").Return(sshclient.Output{})
 	sshClient.On("CreatePostInstallScript", mock.Anything).Return(sshclient.Output{})
-	sshClient.On("ExecuteInstallImage", mock.Anything).Return(sshclient.Output{})
+	sshClient.On("ExecuteInstallImage", mock.Anything).Return(sshclient.Output{StdOut: hostpkg.PostInstallScriptFinished})
 	sshClient.On("Reboot").Return(sshclient.Output{})
 	sshClient.On("GetCloudInitOutput").Return(sshclient.Output{StdOut: "dummy content of /var/log/cloud-init-output.log"})
 	sshClient.On("DetectLinuxOnAnotherDisk", mock.Anything).Return(sshclient.Output{})
diff --git a/controllers/hetznerbaremetalmachine_controller_test.go b/controllers/hetznerbaremetalmachine_controller_test.go
index 00af20fc..b3769b6a 100644
--- a/controllers/hetznerbaremetalmachine_controller_test.go
+++ b/controllers/hetznerbaremetalmachine_controller_test.go
@@ -148,10 +148,6 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
 		configureRescueSSHClient(rescueSSHClient)
 
 		osSSHClient.On("Reboot").Return(sshclient.Output{})
-		osSSHClient.On("CreateNoCloudDirectory").Return(sshclient.Output{})
-		osSSHClient.On("CreateMetaData", mock.Anything).Return(sshclient.Output{})
-		osSSHClient.On("CreateUserData", mock.Anything).Return(sshclient.Output{})
-		osSSHClient.On("EnsureCloudInit").Return(sshclient.Output{StdOut: "cloud-init"})
 		osSSHClient.On("CloudInitStatus").Return(sshclient.Output{StdOut: "status: done"})
 		osSSHClient.On("CheckCloudInitLogsForSigTerm").Return(sshclient.Output{})
 		osSSHClient.On("ResetKubeadm").Return(sshclient.Output{})
diff --git a/controllers/hetznerbaremetalremediation_controller_test.go b/controllers/hetznerbaremetalremediation_controller_test.go
index e77bf665..d7599c0f 100644
--- a/controllers/hetznerbaremetalremediation_controller_test.go
+++ b/controllers/hetznerbaremetalremediation_controller_test.go
@@ -207,10 +207,6 @@ var _ = Describe("HetznerBareMetalRemediationReconciler", func() {
 		configureRescueSSHClient(rescueSSHClient)
 
 		osSSHClientAfterInstallImage.On("Reboot").Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("CreateNoCloudDirectory").Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("CreateMetaData", mock.Anything).Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("CreateUserData", mock.Anything).Return(sshclient.Output{})
-		osSSHClientAfterInstallImage.On("EnsureCloudInit").Return(sshclient.Output{StdOut: "cloud-init"})
 		osSSHClientAfterInstallImage.On("CloudInitStatus").Return(sshclient.Output{StdOut: "status: done"})
 		osSSHClientAfterInstallImage.On("CheckCloudInitLogsForSigTerm").Return(sshclient.Output{})
 		osSSHClientAfterInstallImage.On("ResetKubeadm").Return(sshclient.Output{})
diff --git a/hack/ci-e2e-capi.sh b/hack/ci-e2e-capi.sh
index f4d06554..ecb9c224 100755
--- a/hack/ci-e2e-capi.sh
+++ b/hack/ci-e2e-capi.sh
@@ -33,7 +33,7 @@ SSH_KEY_NAME=caph-e2e-$(
     echo ''
 )
 export SSH_KEY_PATH=/tmp/${SSH_KEY_NAME}
-export HCLOUD_SSH_KEY=${SSH_KEY_NAME}
+export SSH_KEY_NAME=${SSH_KEY_NAME}
 create_ssh_key ${SSH_KEY_NAME} ${SSH_KEY_PATH}
 trap 'remove_ssh_key ${SSH_KEY_NAME}' EXIT
 
diff --git a/pkg/services/baremetal/baremetal/baremetal_test.go b/pkg/services/baremetal/baremetal/baremetal_test.go
index 5d652f63..6773a5a4 100644
--- a/pkg/services/baremetal/baremetal/baremetal_test.go
+++ b/pkg/services/baremetal/baremetal/baremetal_test.go
@@ -337,7 +337,7 @@ var _ = Describe("chooseHost", func() {
 			testCaseChooseHostWithReason{
 				hosts:            []client.Object{&hostWithNonRaidWwnConfig},
 				expectedHostName: "",
-				expectedReason:   "No available host of 1 found: machine-should-use-swraid-but-only-0-RAID-WWN-in-hbmh: 1",
+				expectedReason:   "No available host of 1 found: machine-should-use-swraid-but-not-enough-RAID-WWNs-in-hbmh: 1",
 				swraid:           1,
 			}),
 		Entry("No host, because invalid RAID config (want no RAID)",
diff --git a/pkg/services/baremetal/client/ssh/ssh_client_test.go b/pkg/services/baremetal/client/ssh/ssh_client_test.go
index 55e7da57..6281269f 100644
--- a/pkg/services/baremetal/client/ssh/ssh_client_test.go
+++ b/pkg/services/baremetal/client/ssh/ssh_client_test.go
@@ -17,7 +17,13 @@ limitations under the License.
 // Package sshclient contains the interface to speak to bare metal servers with ssh.
 package sshclient
 
-import "testing"
+import (
+	_ "embed"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
 
 func Test_removeUselessLinesFromCloudInitOutput(t *testing.T) {
 	tests := []struct {
@@ -49,3 +55,50 @@ func Test_removeUselessLinesFromCloudInitOutput(t *testing.T) {
 		})
 	}
 }
+
+func TestOutput_String(t *testing.T) {
+	f := func(o Output, wantString string) {
+		require.Equal(t, wantString, o.String())
+	}
+	f(Output{
+		StdOut: "",
+		StdErr: "",
+		Err:    nil,
+	}, "")
+
+	f(Output{
+		StdOut: " mystdout ",
+		StdErr: "",
+		Err:    nil,
+	}, "mystdout")
+
+	f(Output{
+		StdOut: "",
+		StdErr: " mystderr ",
+		Err:    nil,
+	}, "mystderr")
+
+	f(Output{
+		StdOut: "",
+		StdErr: "",
+		Err:    fmt.Errorf(" some err "),
+	}, "some err")
+
+	f(Output{
+		StdOut: "mystdout",
+		StdErr: "",
+		Err:    fmt.Errorf("some err"),
+	}, "mystdout. Err: some err")
+
+	f(Output{
+		StdOut: "",
+		StdErr: "mystderr",
+		Err:    fmt.Errorf("some err"),
+	}, "mystderr. Err: some err")
+
+	f(Output{
+		StdOut: "mystdout",
+		StdErr: "mystderr",
+		Err:    fmt.Errorf("some err"),
+	}, "mystdout. Stderr: mystderr. Err: some err")
+}
diff --git a/pkg/services/baremetal/host/host_test.go b/pkg/services/baremetal/host/host_test.go
index aaf7ffd9..4fb8021c 100644
--- a/pkg/services/baremetal/host/host_test.go
+++ b/pkg/services/baremetal/host/host_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package host
 
 import (
+	"context"
 	"fmt"
 	"time"
 
@@ -1120,7 +1121,7 @@ var _ = Describe("actionRegistering", func() {
 		expectedErrorMessage      *string
 		swRaid                    bool
 	}
-
+	ctx := context.Background()
 	DescribeTable("actionRegistering",
 		func(tc testCaseActionRegistering) {
 			var host *infrav1.HetznerBareMetalHost
@@ -1155,7 +1156,7 @@ var _ = Describe("actionRegistering", func() {
 			sshMock := registeringSSHMock(tc.storageStdOut)
 			service := newTestService(host, nil, bmmock.NewSSHFactory(sshMock, sshMock, sshMock), nil, helpers.GetDefaultSSHSecret(rescueSSHKeyName, "default"))
 
-			actResult := service.actionRegistering()
+			actResult := service.actionRegistering(ctx)
 			Expect(host.Spec.Status.HardwareDetails).ToNot(BeNil())
 			if tc.expectedErrorMessage != nil {
 				Expect(host.Spec.Status.ErrorMessage).To(Equal(*tc.expectedErrorMessage))
@@ -1231,7 +1232,7 @@ var _ = Describe("actionRegistering", func() {
 
 			service := newTestService(host, &robotMock, bmmock.NewSSHFactory(sshMock, sshMock, sshMock), nil, helpers.GetDefaultSSHSecret(rescueSSHKeyName, "default"))
 
-			actResult := service.actionRegistering()
+			actResult := service.actionRegistering(ctx)
 			Expect(actResult).Should(BeAssignableToTypeOf(actionContinue{}))
 			if tc.expectedErrorType != infrav1.ErrorType("") {
 				Expect(host.Spec.Status.ErrorType).To(Equal(tc.expectedErrorType))
@@ -1270,6 +1271,7 @@ name="eth0" model="Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express
 }
 
 var _ = Describe("actionRegistering check RAID", func() {
+	ctx := context.Background()
 	It("check RAID", func() {
 		sshMock := registeringSSHMock(`NAME="nvme2n1" TYPE="disk" MODEL="mymode." VENDOR="" SIZE="3068773888" WWN="wwn1" ROTA="0"
 		NAME="nvme2n2" TYPE="disk" MODEL="mymodel" VENDOR="" SIZE="3068773888" WWN="wwn2" ROTA="0"`)
@@ -1285,7 +1287,7 @@ var _ = Describe("actionRegistering check RAID", func() {
 		}
 		service := newTestService(host, nil, bmmock.NewSSHFactory(
 			sshMock, sshMock, sshMock), nil, helpers.GetDefaultSSHSecret(rescueSSHKeyName, "default"))
-		actResult := service.actionRegistering()
+		actResult := service.actionRegistering(ctx)
 
 		_, err := actResult.Result()
 		Expect(err).Should(BeNil())
@@ -1294,7 +1296,7 @@ var _ = Describe("actionRegistering check RAID", func() {
 		host.Spec.Status.InstallImage.Swraid = 0
 		host.Spec.RootDeviceHints.WWN = ""
 		host.Spec.RootDeviceHints.Raid.WWN = []string{"wwn1", "wwn2"}
-		actResult = service.actionRegistering()
+		actResult = service.actionRegistering(ctx)
 
 		_, err = actResult.Result()
 		Expect(err).Should(BeNil())
@@ -1390,6 +1392,7 @@ var _ = Describe("actionEnsureProvisioned", func() {
 
 	DescribeTable("actionEnsureProvisioned",
 		func(in testCaseActionEnsureProvisioned) {
+			ctx := context.Background()
 			var (
 				portAfterCloudInit    = 24
 				portAfterInstallImage = 23
@@ -1427,7 +1430,7 @@ var _ = Describe("actionEnsureProvisioned", func() {
 
 			service := newTestService(host, &robotMock, bmmock.NewSSHFactory(sshMock, oldSSHMock, sshMock), helpers.GetDefaultSSHSecret(osSSHKeyName, "default"), nil)
 
-			actResult := service.actionEnsureProvisioned()
+			actResult := service.actionEnsureProvisioned(ctx)
 			Expect(actResult).Should(BeAssignableToTypeOf(in.expectedActionResult))
 			if in.expectedErrorType != infrav1.ErrorType("") {
 				Expect(host.Spec.Status.ErrorType).To(Equal(in.expectedErrorType))
@@ -1748,6 +1751,7 @@ var _ = Describe("actionProvisioned", func() {
 
 	DescribeTable("actionProvisioned",
 		func(tc testCaseActionProvisioned) {
+			ctx := context.Background()
 			host := helpers.BareMetalHost(
 				"test-host",
 				"default",
@@ -1774,7 +1778,7 @@ var _ = Describe("actionProvisioned", func() {
 
 			service := newTestService(host, nil, bmmock.NewSSHFactory(sshMock, sshMock, sshMock), helpers.GetDefaultSSHSecret(osSSHKeyName, "default"), helpers.GetDefaultSSHSecret(rescueSSHKeyName, "default"))
 
-			actResult := service.actionProvisioned()
+			actResult := service.actionProvisioned(ctx)
 			Expect(actResult).Should(BeAssignableToTypeOf(tc.expectedActionResult))
 			Expect(host.Spec.Status.Rebooted).To(Equal(tc.expectRebootInStatus))
 			Expect(host.HasRebootAnnotation()).To(Equal(tc.expectRebootAnnotation))
diff --git a/pkg/services/baremetal/host/state_machine_test.go b/pkg/services/baremetal/host/state_machine_test.go
index a2b5819b..50453ecb 100644
--- a/pkg/services/baremetal/host/state_machine_test.go
+++ b/pkg/services/baremetal/host/state_machine_test.go
@@ -189,7 +189,7 @@ var _ = Describe("updateSSHKey", func() {
 				"sshkey-name": []byte("my-name"),
 				"public-key":  []byte("my-public-key"),
 			},
-			currentState:         infrav1.StateProvisioning,
+			currentState:         infrav1.StateImageInstalling,
 			expectedActionResult: actionComplete{},
 			expectedNextState:    infrav1.StateImageInstalling,
 			expectedOSSecretData: map[string][]byte{
@@ -203,31 +203,6 @@ var _ = Describe("updateSSHKey", func() {
 				"public-key":  []byte("my-public-key"),
 			},
 		}),
-		Entry("rescue secret changed - state provisioning", testCaseUpdateSSHKey{
-			osSecretData: map[string][]byte{
-				"private-key": []byte(fmt.Sprintf("%s-private-key", osSSHKeyName)),
-				"sshkey-name": []byte("my-name"),
-				"public-key":  []byte("my-public-key"),
-			},
-			rescueSecretData: map[string][]byte{
-				"private-key": []byte(fmt.Sprintf("%s-private-key", rescueSSHKeyName)),
-				"sshkey-name": []byte("my-old-name"),
-				"public-key":  []byte("my-old-public-key"),
-			},
-			currentState:         infrav1.StateProvisioning,
-			expectedActionResult: actionComplete{},
-			expectedNextState:    infrav1.StateProvisioning,
-			expectedOSSecretData: map[string][]byte{
-				"private-key": []byte(fmt.Sprintf("%s-private-key", osSSHKeyName)),
-				"sshkey-name": []byte("my-name"),
-				"public-key":  []byte("my-public-key"),
-			},
-			expectedRescueSecretData: map[string][]byte{
-				"private-key": []byte(fmt.Sprintf("%s-private-key", rescueSSHKeyName)),
-				"sshkey-name": []byte("my-name"),
-				"public-key":  []byte("my-public-key"),
-			},
-		}),
 		Entry("rescue secret changed - state available", testCaseUpdateSSHKey{
 			osSecretData: map[string][]byte{
 				"private-key": []byte(fmt.Sprintf("%s-private-key", osSSHKeyName)),
diff --git a/test/e2e/config/hetzner-ci.yaml b/test/e2e/config/hetzner-ci.yaml
index d9b31caa..be2bad5b 100644
--- a/test/e2e/config/hetzner-ci.yaml
+++ b/test/e2e/config/hetzner-ci.yaml
@@ -147,7 +147,7 @@ variables:
   HETZNER_ROBOT_PASSWORD: "secret"
   HETZNER_SSH_PUB: "secret"
   HETZNER_SSH_PRIV: "secret"
-  HCLOUD_SSH_KEY: "test"
+  SSH_KEY_NAME: "test"
   HCLOUD_REGION: "nbg1"
   HCLOUD_IMAGE_NAME: ${HCLOUD_IMAGE_NAME:-}
   HCLOUD_CONTROL_PLANE_MACHINE_TYPE: cpx31
diff --git a/test/e2e/config/hetzner.yaml b/test/e2e/config/hetzner.yaml
index 710abb1c..791e7525 100644
--- a/test/e2e/config/hetzner.yaml
+++ b/test/e2e/config/hetzner.yaml
@@ -153,7 +153,7 @@ variables:
   HETZNER_ROBOT_PASSWORD: "secret"
   HETZNER_SSH_PUB: "secret"
   HETZNER_SSH_PRIV: "secret"
-  HCLOUD_SSH_KEY: "test"
+  SSH_KEY_NAME: "test"
   HCLOUD_REGION: "nbg1"
   HCLOUD_IMAGE_NAME: ${HCLOUD_IMAGE_NAME:-}
   HCLOUD_CONTROL_PLANE_MACHINE_TYPE: cpx31
diff --git a/test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts-raid.yaml b/test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts-raid.yaml
index 9b318239..49e7d352 100644
--- a/test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts-raid.yaml
+++ b/test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts-raid.yaml
@@ -1,7 +1,7 @@
 apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 kind: HetznerBareMetalHost
 metadata:
-  name: "bm-2"
+  name: "bm-e2e-raid-1716772"
 spec:
   serverID: 1716772
   rootDeviceHints:
@@ -15,7 +15,7 @@ spec:
 apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 kind: HetznerBareMetalHost
 metadata:
-  name: "bm-6"
+  name: "bm-e2e-raid-1763731"
 spec:
   serverID: 1763731
   rootDeviceHints:
diff --git a/test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts.yaml b/test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts.yaml
index 63b6a81c..9689ca78 100644
--- a/test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts.yaml
+++ b/test/e2e/data/infrastructure-hetzner/v1beta1/bases/hetznerbaremetalhosts.yaml
@@ -1,66 +1,66 @@
 apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 kind: HetznerBareMetalHost
 metadata:
-  name: "bm-1"
+  name: "bm-e2e-1670788"
 spec:
   serverID: 1670788
   rootDeviceHints:
     wwn: "0x5002538c0004413f"
   maintenanceMode: false
-  description: Test Machine 1
+  description: Test Machine 1670788
 ---
 apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 kind: HetznerBareMetalHost
 metadata:
-  name: "bm-2"
+  name: "bm-e2e-1716772"
 spec:
   serverID: 1716772
   rootDeviceHints:
     wwn: "0x500a07510fe0ce34"
   maintenanceMode: false
-  description: Test Machine 2
+  description: Test Machine 1716772
 ---
 apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 kind: HetznerBareMetalHost
 metadata:
-  name: "bm-3"
+  name: "bm-e2e-1724024"
 spec:
   serverID: 1724024
   rootDeviceHints:
     wwn: "0x500a075111968d25"
   maintenanceMode: false
-  description: Test Machine 3
-# --- We deactived bm-4 since it can't reach the rescue system.
+  description: Test Machine 1724024
+# --- We deactived 1731561 since it can't reach the rescue system.
 #     We enable it again, if we solved the issue.
 # apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 # kind: HetznerBareMetalHost
 # metadata:
-#   name: "bm-4"
+#   name: "bm-e2e-1731561"
 # spec:
 #   serverID: 1731561
 #   rootDeviceHints:
 #     wwn: "0x500a075119615dc2"
 #   maintenanceMode: false
-#   description: Test Machine 4
+#   description: Test Machine 1731561
 # ---
 # apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 # kind: HetznerBareMetalHost
 # metadata:
-#   name: "bm-5"
+#   name: "bm-e2e-1751550"
 # spec:
-#   serverID: 1751550 # this is the identifier in hetzner UI
+#   serverID: 1751550
 #   rootDeviceHints:
 #     wwn: "0x500a075119549dcf"
 #   maintenanceMode: false
-#   description: Test Machine 5
+#   description: Test Machine 1751550
 # ---
 # apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 # kind: HetznerBareMetalHost
 # metadata:
-#   name: "bm-6"
+#   name: "bm-e2e-1763731"
 # spec:
 #   serverID: 1763731
 #   rootDeviceHints:
 #     wwn: "0x500a07511756c36d"
 #   maintenanceMode: false
-#   description: Test Machine 6
+#   description: Test Machine 1751550