Repository: syself/cluster-api-provider-hetzner
Pull request #1392 (2024-07-23 11:06:33)

:seedling: Update image docker.io/library/alpine to v3.20.2
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| docker.io/library/alpine | final | patch | `3.20.1` -> `3.20.2` |
| docker.io/library/alpine | stage | patch | `3.20.1` -> `3.20.2` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40NDAuMSIsInVwZGF0ZWRJblZlciI6IjM3LjQ0MC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJ0eXBlL3BhdGNoIiwidXBkYXRlL2NvbnRhaW5lciJdfQ==-->



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/.lychee.toml b/.lychee.toml
index 1422d31f..68f420d5 100644
--- a/.lychee.toml
+++ b/.lychee.toml
@@ -58,6 +58,7 @@ exclude = [
     "https://www.hetzner.com/*",
     "https://opensource.org/license/apache-2-0/",
     "https://robot.your-server.de/*",
+    "https://www.linkedin.com/company/syself/",
 ]
 
 include = []
@@ -80,4 +81,4 @@ exclude_mail = false
 
 # Avoid https://docs.hetzner.com/... | Failed: Network error: Connection reset by peer (os error 104)
 max_concurrency = 3
-retry_wait_time = 3
\ No newline at end of file
+retry_wait_time = 3
diff --git a/Makefile b/Makefile
index 2e9f6940..702b89b9 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
@@ -744,6 +744,7 @@ ifeq ($(BUILD_IN_CONTAINER),true)
 		-v $(shell pwd):/src/cluster-api-provider-$(INFRA_PROVIDER)$(MOUNT_FLAGS) \
 		$(BUILDER_IMAGE):$(BUILDER_IMAGE_VERSION) $@;
 else
+	@lychee --version
 	lychee --config .lychee.toml ./*.md  ./docs/**/*.md
 endif
 
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/conditions_const.go b/api/v1beta1/conditions_const.go
index 6bc272f1..bcc05436 100644
--- a/api/v1beta1/conditions_const.go
+++ b/api/v1beta1/conditions_const.go
@@ -48,6 +48,10 @@ const (
 	ServerLimitExceededReason = "ServerLimitExceeded"
 	// ServerTypeNotFoundReason indicates that server type could not be found.
 	ServerTypeNotFoundReason = "ServerTypeNotFound"
+	// ServerResourceUnavailableReason indicates that server resource is unavailable.
+	ServerResourceUnavailableReason = "ServerResourceUnavailable"
+	// ServerPlacementErrorReason indicates placement error while creating server.
+	ServerPlacementErrorReason = "ServerPlacementError"
 )
 
 const (
diff --git a/api/v1beta1/hcloudmachinetemplate_webhook.go b/api/v1beta1/hcloudmachinetemplate_webhook.go
index 393302ef..a616aef1 100644
--- a/api/v1beta1/hcloudmachinetemplate_webhook.go
+++ b/api/v1beta1/hcloudmachinetemplate_webhook.go
@@ -71,8 +71,6 @@ func (r *HCloudMachineTemplateWebhook) ValidateUpdate(ctx context.Context, oldRa
 		allErrs = append(allErrs, field.Invalid(field.NewPath("spec"), newHCloudMachineTemplate, "HCloudMachineTemplate.Spec is immutable"))
 	}
 
-	allErrs = append(allErrs, validateHCloudMachineSpec(oldHCloudMachineTemplate.Spec.Template.Spec, newHCloudMachineTemplate.Spec.Template.Spec)...)
-
 	return nil, aggregateObjErrors(newHCloudMachineTemplate.GroupVersionKind().GroupKind(), newHCloudMachineTemplate.Name, allErrs)
 }
 
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/api/v1beta1/hetznerbaremetalmachinetemplate_webhook.go b/api/v1beta1/hetznerbaremetalmachinetemplate_webhook.go
index 21166c43..e8c4ec7e 100644
--- a/api/v1beta1/hetznerbaremetalmachinetemplate_webhook.go
+++ b/api/v1beta1/hetznerbaremetalmachinetemplate_webhook.go
@@ -57,9 +57,9 @@ func (r *HetznerBareMetalMachineTemplateWebhook) ValidateCreate(_ context.Contex
 		hbmmt.Spec.Template.Spec.SSHSpec.PortAfterCloudInit = hbmmt.Spec.Template.Spec.SSHSpec.PortAfterInstallImage
 	}
 
-	allErrs := validateHetznerBareMetalMachineSpecCreate(hbmmt.Spec.Template.Spec)
-
-	return nil, aggregateObjErrors(hbmmt.GroupVersionKind().GroupKind(), hbmmt.Name, allErrs)
+	// TODO: Cannot validate it because ClusterClass applies empty template objects
+	// allErrs := validateHetznerBareMetalMachineSpecCreate(hbmmt.Spec.Template.Spec)
+	return nil, nil
 }
 
 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
@@ -83,8 +83,6 @@ func (r *HetznerBareMetalMachineTemplateWebhook) ValidateUpdate(ctx context.Cont
 		allErrs = append(allErrs, field.Invalid(field.NewPath("spec"), newHetznerBareMetalMachineTemplate, "HetznerBareMetalMachineTemplate.Spec is immutable"))
 	}
 
-	allErrs = append(allErrs, validateHetznerBareMetalMachineSpecUpdate(oldHetznerBareMetalMachineTemplate.Spec.Template.Spec, newHetznerBareMetalMachineTemplate.Spec.Template.Spec)...)
-
 	return nil, aggregateObjErrors(newHetznerBareMetalMachineTemplate.GroupVersionKind().GroupKind(), newHetznerBareMetalMachineTemplate.Name, allErrs)
 }
 
diff --git a/api/v1beta1/hetznercluster_webhook.go b/api/v1beta1/hetznercluster_webhook.go
index 0d4c5a18..3904e459 100644
--- a/api/v1beta1/hetznercluster_webhook.go
+++ b/api/v1beta1/hetznercluster_webhook.go
@@ -39,6 +39,7 @@ var regionNetworkZoneMap = map[string]string{
 	"hel1": "eu-central",
 	"ash":  "us-east",
 	"hil":  "us-west",
+	"sin":  "ap-southeast",
 }
 
 // SetupWebhookWithManager initializes webhook manager for HetznerCluster.
@@ -91,7 +92,7 @@ func (r *HetznerCluster) ValidateCreate() (admission.Warnings, error) {
 			allErrs = append(allErrs, field.Invalid(
 				field.NewPath("spec", "controlPlaneRegions"),
 				r.Spec.ControlPlaneRegions,
-				"wrong control plane region. Should be fsn1, nbg1, hel1, or ash",
+				"wrong control plane region. Should be fsn1, nbg1, hel1, ash, hil or sin",
 			))
 		}
 	}
diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go
index 229e31ed..f574a794 100644
--- a/api/v1beta1/types.go
+++ b/api/v1beta1/types.go
@@ -233,7 +233,7 @@ type HCloudNetworkSpec struct {
 
 	// NetworkZone specifies the HCloud network zone of the private network.
 	// The zones must be one of eu-central, us-east, or us-west. The default is eu-central.
-	// +kubebuilder:validation:Enum=eu-central;us-east;us-west
+	// +kubebuilder:validation:Enum=eu-central;us-east;us-west;ap-southeast
 	// +kubebuilder:default=eu-central
 	// +optional
 	NetworkZone HCloudNetworkZone `json:"networkZone,omitempty"`
@@ -247,7 +247,7 @@ type NetworkStatus struct {
 }
 
 // Region is a Hetzner Location.
-// +kubebuilder:validation:Enum=fsn1;hel1;nbg1;ash;hil
+// +kubebuilder:validation:Enum=fsn1;hel1;nbg1;ash;hil;sin
 type Region string
 
 // HCloudNetworkZone describes the Network zone.
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hcloudmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hcloudmachines.yaml
index d5427343..c063cd9e 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hcloudmachines.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hcloudmachines.yaml
@@ -253,6 +253,7 @@ spec:
                 - nbg1
                 - ash
                 - hil
+                - sin
                 type: string
               sshKeys:
                 description: SSHKeys specifies the ssh keys that were used for provisioning
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/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml
index 71791313..cea2fdce 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml
@@ -157,6 +157,7 @@ spec:
                     - nbg1
                     - ash
                     - hil
+                    - sin
                     type: string
                   type:
                     default: lb11
@@ -181,6 +182,7 @@ spec:
                   - nbg1
                   - ash
                   - hil
+                  - sin
                   type: string
                 type: array
               hcloudNetwork:
@@ -205,6 +207,7 @@ spec:
                     - eu-central
                     - us-east
                     - us-west
+                    - ap-southeast
                     type: string
                   subnetCidrBlock:
                     default: 10.0.0.0/24
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml
index 692139b7..6661bd46 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml
@@ -187,6 +187,7 @@ spec:
                             - nbg1
                             - ash
                             - hil
+                            - sin
                             type: string
                           type:
                             default: lb11
@@ -211,6 +212,7 @@ spec:
                           - nbg1
                           - ash
                           - hil
+                          - sin
                           type: string
                         type: array
                       hcloudNetwork:
@@ -236,6 +238,7 @@ spec:
                             - eu-central
                             - us-east
                             - us-west
+                            - ap-southeast
                             type: string
                           subnetCidrBlock:
                             default: 10.0.0.0/24
diff --git a/controllers/hetznerbaremetalhost_controller.go b/controllers/hetznerbaremetalhost_controller.go
index 92579bc7..2f45b95e 100644
--- a/controllers/hetznerbaremetalhost_controller.go
+++ b/controllers/hetznerbaremetalhost_controller.go
@@ -67,6 +67,15 @@ type HetznerBareMetalHostReconciler struct {
 func (r *HetznerBareMetalHostReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, reterr error) {
 	log := ctrl.LoggerFrom(ctx)
 
+	start := time.Now()
+	defer func() {
+		// check duration of reconcile. Warn if it took too long.
+		duration := time.Since(start)
+		if duration > 15*time.Second {
+			log.Info("Reconcile took too long", "duration", duration, "res", res, "reterr", reterr)
+		}
+	}()
+
 	// Fetch the Hetzner bare metal host instance.
 	bmHost := &infrav1.HetznerBareMetalHost{}
 	err := r.Get(ctx, req.NamespacedName, bmHost)
@@ -77,8 +86,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..a76ea2fb 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -5,31 +5,31 @@ 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)
+- [Managing SSH Keys](/docs/caph/02-topics/01-managing-ssh-keys.md)
+- [High Availability Setup](/docs/caph/02-topics/02-HA-setup.md)
+- [Node Image](/docs/caph/02-topics/03-node-image.md)
+- [Upgrading CAPH](/docs/caph/02-topics/04-upgrading-caph.md)
+- [Hetzner Baremetal](/docs/caph/02-topics/05-baremetal)
+- [Advanced CAPH](/docs/caph/02-topics/06-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..481efee3 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/01-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/02-quickstart/03-creating-a-workload-cluster.md b/docs/caph/01-getting-started/02-quickstart/03-creating-a-workload-cluster.md
index a2204836..9b6bcab8 100644
--- a/docs/caph/01-getting-started/02-quickstart/03-creating-a-workload-cluster.md
+++ b/docs/caph/01-getting-started/02-quickstart/03-creating-a-workload-cluster.md
@@ -97,10 +97,10 @@ The following `make` command will install the CCM in your workload cluster:
 For a cluster without a private network, use the following command:
 
 ```shell
-helm repo add syself https://charts.syself.com
-helm repo update syself
+helm repo add hcloud https://charts.hetzner.cloud
+helm repo update hcloud
 
-KUBECONFIG=$CAPH_WORKER_CLUSTER_KUBECONFIG helm upgrade --install ccm syself/ccm-hcloud --version 1.0.11 \
+KUBECONFIG=$CAPH_WORKER_CLUSTER_KUBECONFIG helm upgrade --install hccm hcloud/hcloud-cloud-controller-manager --version 1.20.0 \
 	--namespace kube-system \
 	--set secret.name=hetzner \
 	--set secret.tokenKeyName=hcloud \
diff --git a/docs/caph/01-getting-started/03-preparation.md b/docs/caph/01-getting-started/03-preparation.md
index f0e15b77..c3918b65 100644
--- a/docs/caph/01-getting-started/03-preparation.md
+++ b/docs/caph/01-getting-started/03-preparation.md
@@ -1,18 +1,16 @@
 ---
-title: Preparation
+title: Hetzner project preparation
 ---
 
-## Preparation of the Hetzner Project and Credentials
-
 There are several tasks that have to be completed before a workload cluster can be created.
 
-### Preparing Hetzner Cloud
+## Preparing Hetzner Cloud
 
 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/01-managing-ssh-keys.md).
 
-### Preparing Hetzner Robot
+## Preparing Hetzner Robot
 
 1. Create a new web service user. [Here](https://robot.your-server.de/doc/webservice/en.html#preface) you can define a password and copy your user name
 2. Generate an SSH key. You can either upload it via Hetzner Robot UI or just rely on the controller to upload a key that it does not find in the robot API. This is possible, as you have to store the public and private key together with the SSH key's name in a secret that the controller reads.
@@ -32,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
 
@@ -65,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 \
@@ -78,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`
 
@@ -90,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:
@@ -104,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.
 
@@ -147,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/02-node-image) for more information.
+See [node-image](/docs/caph/02-topics/03-node-image.md) for more information.
diff --git a/docs/caph/02-topics/02-managing-ssh-keys.md b/docs/caph/02-topics/01-managing-ssh-keys.md
similarity index 100%
rename from docs/caph/02-topics/02-managing-ssh-keys.md
rename to docs/caph/02-topics/01-managing-ssh-keys.md
diff --git a/docs/caph/02-topics/01-preparation.md b/docs/caph/02-topics/01-preparation.md
deleted file mode 100644
index c5626285..00000000
--- a/docs/caph/02-topics/01-preparation.md
+++ /dev/null
@@ -1,148 +0,0 @@
----
-title: Hetzner project preparation
----
-
-There are several tasks that have to be completed before a workload cluster can be created.
-
-## Preparing Hetzner Cloud
-
-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).
-
-## Preparing Hetzner Robot
-
-1. Create a new web service user. [Here](https://robot.your-server.de/doc/webservice/en.html#preface) you can define a password and copy your user name
-2. Generate an SSH key. You can either upload it via Hetzner Robot UI or just rely on the controller to upload a key that it does not find in the robot API. This is possible, as you have to store the public and private key together with the SSH key's name in a secret that the controller reads.
-
----
-
-## Bootstrap or Management Cluster Installation
-
-### Common Prerequisites
-
-- Install and set kubectl up in your local environment
-- Install [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) and [Docker](https://docs.docker.com/engine/install/)
-
-### Install and configure a Kubernetes cluster
-
-Cluster API requires an existing Kubernetes cluster accessible via kubectl. During the installation process, the Kubernetes cluster will be transformed into a management cluster by installing the Cluster API provider components, so it is recommended to keep it separated from any application workload.
-
-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:
-
-#### 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).
-
-#### 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.
-
----
-
-## Install Clusterctl and initialize Management Cluster
-
-### Install Clusterctl
-
-Please use the instructions in the official [Cluster API docs](https://cluster-api.sigs.k8s.io/user/quick-start.html#install-clusterctl), or use this command: `make clusterctl`
-
-### Initialize the management cluster
-
-Now that we’ve got clusterctl installed and all the prerequisites are in place, we can transform the Kubernetes cluster into a management cluster by using the `clusterctl init` command. More information about clusterctl can be found [here](https://cluster-api.sigs.k8s.io/clusterctl/commands/commands.html).
-
-For the latest version:
-
-```shell
-clusterctl init --core cluster-api --bootstrap kubeadm --control-plane kubeadm --infrastructure hetzner
-
-```
-
-Or for a specific [version](https://github.com/syself/cluster-api-provider-hetzner/releases): `--infrastructure hetzner:vX.X.X`
-
----
-
-## Variable Preparation to generate a cluster-template.
-
-```shell
-export HCLOUD_SSH_KEY="<ssh-key-name>" \
-export CLUSTER_NAME="my-cluster" \
-export HCLOUD_REGION="fsn1" \
-export CONTROL_PLANE_MACHINE_COUNT=3 \
-export WORKER_MACHINE_COUNT=3 \
-export KUBERNETES_VERSION=1.29.4 \
-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/
-- HCLOUD_IMAGE_NAME: The Image name of your operating system.
-- 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`
-
-```shell
-$ clusterctl generate cluster --infrastructure hetzner:<caph-version> --list-variables hetzner-cluster
-Required Variables:
-  - HCLOUD_CONTROL_PLANE_MACHINE_TYPE
-  - HCLOUD_REGION
-  - HCLOUD_SSH_KEY
-  - HCLOUD_WORKER_MACHINE_TYPE
-
-Optional Variables:
-  - CLUSTER_NAME                 (defaults to hetzner-cluster)
-  - CONTROL_PLANE_MACHINE_COUNT  (defaults to 3)
-  - KUBERNETES_VERSION           (defaults to 1.29.4)
-  - WORKER_MACHINE_COUNT         (defaults to 3)
-```
-
-### Create a secret for hcloud only
-
-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>" `
-
-- HCLOUD_TOKEN: The project where your cluster will be placed. You have to get a token from your HCloud Project.
-
-```shell
-kubectl create secret generic hetzner --from-literal=hcloud=$HCLOUD_TOKEN
-
-# Patch the created secret so it is automatically moved to the target cluster later.
-kubectl patch secret hetzner -p '{"metadata":{"labels":{"clusterctl.cluster.x-k8s.io/move":""}}}'
-```
-
-The secret name and the tokens can also be customized in the cluster template.
-
-### Create a secret for Hetzner (Hcloud + Robot)
-
-In order for the provider integration hetzner to communicate with the Hetzner API ([HCloud API](https://docs.hetzner.cloud/) + [Robot API](https://robot.your-server.de/doc/webservice/en.html#preface)), we need to create a secret with the access data. The secret must be in the same namespace as the other CRs.
-
-```shell
-export HCLOUD_TOKEN="<YOUR-TOKEN>" \
-export HETZNER_ROBOT_USER="<YOUR-ROBOT-USER>" \
-export HETZNER_ROBOT_PASSWORD="<YOUR-ROBOT-PASSWORD>" \
-export HETZNER_SSH_PUB_PATH="<YOUR-SSH-PUBLIC-PATH>" \
-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/Webservice and app settings.
-- HETZNER_ROBOT_PASSWORD: The Robot Password you have set in Robot under settings/Webservice and app settings.
-- 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=cluster --from-file=ssh-privatekey=$HETZNER_SSH_PRIV_PATH --from-file=ssh-publickey=$HETZNER_SSH_PUB_PATH
-
-# Patch the created secrets so that they get automatically moved to the target cluster later.
-kubectl patch secret hetzner -p '{"metadata":{"labels":{"clusterctl.cluster.x-k8s.io/move":""}}}'
-kubectl patch secret robot-ssh -p '{"metadata":{"labels":{"clusterctl.cluster.x-k8s.io/move":""}}}'
-```
-
-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.
diff --git a/docs/caph/02-topics/03-HA-setup.md b/docs/caph/02-topics/02-HA-setup.md
similarity index 100%
rename from docs/caph/02-topics/03-HA-setup.md
rename to docs/caph/02-topics/02-HA-setup.md
diff --git a/docs/caph/02-topics/04-node-image.md b/docs/caph/02-topics/03-node-image.md
similarity index 100%
rename from docs/caph/02-topics/04-node-image.md
rename to docs/caph/02-topics/03-node-image.md
diff --git a/docs/caph/02-topics/05-upgrading-caph.md b/docs/caph/02-topics/04-upgrading-caph.md
similarity index 100%
rename from docs/caph/02-topics/05-upgrading-caph.md
rename to docs/caph/02-topics/04-upgrading-caph.md
diff --git a/docs/caph/02-topics/06-baremetal/01-introduction.md b/docs/caph/02-topics/05-baremetal/01-introduction.md
similarity index 94%
rename from docs/caph/02-topics/06-baremetal/01-introduction.md
rename to docs/caph/02-topics/05-baremetal/01-introduction.md
index f86c8e75..573361b0 100644
--- a/docs/caph/02-topics/06-baremetal/01-introduction.md
+++ b/docs/caph/02-topics/05-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/05-baremetal/02-management-cluster.md
similarity index 93%
rename from docs/caph/02-topics/06-baremetal/02-management-cluster.md
rename to docs/caph/02-topics/05-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/05-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/06-baremetal/03-creating-workload-cluster.md b/docs/caph/02-topics/05-baremetal/03-creating-workload-cluster.md
similarity index 100%
rename from docs/caph/02-topics/06-baremetal/03-creating-workload-cluster.md
rename to docs/caph/02-topics/05-baremetal/03-creating-workload-cluster.md
diff --git a/docs/caph/02-topics/06-baremetal/04-constant-hostnames.md b/docs/caph/02-topics/05-baremetal/04-constant-hostnames.md
similarity index 100%
rename from docs/caph/02-topics/06-baremetal/04-constant-hostnames.md
rename to docs/caph/02-topics/05-baremetal/04-constant-hostnames.md
diff --git a/docs/caph/02-topics/07-advanced/01-csr-controller.md b/docs/caph/02-topics/06-advanced/01-csr-controller.md
similarity index 100%
rename from docs/caph/02-topics/07-advanced/01-csr-controller.md
rename to docs/caph/02-topics/06-advanced/01-csr-controller.md
diff --git a/docs/caph/02-topics/07-advanced/02-rate-limits.md b/docs/caph/02-topics/06-advanced/02-rate-limits.md
similarity index 100%
rename from docs/caph/02-topics/07-advanced/02-rate-limits.md
rename to docs/caph/02-topics/06-advanced/02-rate-limits.md
diff --git a/docs/caph/02-topics/07-advanced/03-multi-tenancy.md b/docs/caph/02-topics/06-advanced/03-multi-tenancy.md
similarity index 100%
rename from docs/caph/02-topics/07-advanced/03-multi-tenancy.md
rename to docs/caph/02-topics/06-advanced/03-multi-tenancy.md
diff --git a/docs/caph/02-topics/07-advanced/04-custom-templates-mhc.md b/docs/caph/02-topics/06-advanced/04-custom-templates-mhc.md
similarity index 89%
rename from docs/caph/02-topics/07-advanced/04-custom-templates-mhc.md
rename to docs/caph/02-topics/06-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/06-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..da5f7484 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/01-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..5f9bf39b 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/03-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..d52e17f5 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/06-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..e33848cd 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/01-getting-started/03-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/01-getting-started/03-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/images/cache/Dockerfile b/images/cache/Dockerfile
index e866aeb9..c8ede9c7 100644
--- a/images/cache/Dockerfile
+++ b/images/cache/Dockerfile
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 
-FROM docker.io/library/alpine:3.20.1@sha256:b89d9c93e9ed3597455c90a0b88a8bbb5cb7188438f70953fede212a0c4394e0 AS import-cache
+FROM docker.io/library/alpine:3.20.2@sha256:0a4eaa0eecf5f8c050e5bba433f58c052be7587ee8af3e8b3910ef9ab5fbe9f5 AS import-cache
 
 RUN --mount=type=bind,target=/host-tmp \
     --mount=type=cache,target=/root/.cache \
@@ -27,7 +27,7 @@ RUN --mount=type=bind,target=/host-tmp \
       tar xzf /host-tmp/go-pkg-cache.tar.gz --no-same-owner -C /go/pkg; \
     fi
 
-FROM docker.io/library/alpine:3.20.1@sha256:b89d9c93e9ed3597455c90a0b88a8bbb5cb7188438f70953fede212a0c4394e0 AS cache-creator
+FROM docker.io/library/alpine:3.20.2@sha256:0a4eaa0eecf5f8c050e5bba433f58c052be7587ee8af3e8b3910ef9ab5fbe9f5 AS cache-creator
 RUN --mount=type=cache,target=/root/.cache \
     --mount=type=cache,target=/go/pkg \
     tar czf /tmp/go-build-cache.tar.gz -C /root/.cache/go-build . ; \
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..213951ce 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)
@@ -1153,47 +971,112 @@ func (_c *Client_GetHostName_Call) RunAndReturn(run func() sshclient.Output) *Cl
 	return _c
 }
 
-// GetRunningInstallImageProcesses provides a mock function with given fields:
-func (_m *Client) GetRunningInstallImageProcesses() sshclient.Output {
+// GetInstallImageState provides a mock function with given fields:
+func (_m *Client) GetInstallImageState() (sshclient.InstallImageState, error) {
 	ret := _m.Called()
 
 	if len(ret) == 0 {
-		panic("no return value specified for GetRunningInstallImageProcesses")
+		panic("no return value specified for GetInstallImageState")
 	}
 
-	var r0 sshclient.Output
-	if rf, ok := ret.Get(0).(func() sshclient.Output); ok {
+	var r0 sshclient.InstallImageState
+	var r1 error
+	if rf, ok := ret.Get(0).(func() (sshclient.InstallImageState, error)); ok {
+		return rf()
+	}
+	if rf, ok := ret.Get(0).(func() sshclient.InstallImageState); ok {
 		r0 = rf()
 	} else {
-		r0 = ret.Get(0).(sshclient.Output)
+		r0 = ret.Get(0).(sshclient.InstallImageState)
 	}
 
-	return r0
+	if rf, ok := ret.Get(1).(func() error); ok {
+		r1 = rf()
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
 }
 
-// Client_GetRunningInstallImageProcesses_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRunningInstallImageProcesses'
-type Client_GetRunningInstallImageProcesses_Call struct {
+// Client_GetInstallImageState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetInstallImageState'
+type Client_GetInstallImageState_Call struct {
 	*mock.Call
 }
 
-// GetRunningInstallImageProcesses is a helper method to define mock.On call
-func (_e *Client_Expecter) GetRunningInstallImageProcesses() *Client_GetRunningInstallImageProcesses_Call {
-	return &Client_GetRunningInstallImageProcesses_Call{Call: _e.mock.On("GetRunningInstallImageProcesses")}
+// GetInstallImageState is a helper method to define mock.On call
+func (_e *Client_Expecter) GetInstallImageState() *Client_GetInstallImageState_Call {
+	return &Client_GetInstallImageState_Call{Call: _e.mock.On("GetInstallImageState")}
 }
 
-func (_c *Client_GetRunningInstallImageProcesses_Call) Run(run func()) *Client_GetRunningInstallImageProcesses_Call {
+func (_c *Client_GetInstallImageState_Call) Run(run func()) *Client_GetInstallImageState_Call {
 	_c.Call.Run(func(args mock.Arguments) {
 		run()
 	})
 	return _c
 }
 
-func (_c *Client_GetRunningInstallImageProcesses_Call) Return(_a0 sshclient.Output) *Client_GetRunningInstallImageProcesses_Call {
-	_c.Call.Return(_a0)
+func (_c *Client_GetInstallImageState_Call) Return(_a0 sshclient.InstallImageState, _a1 error) *Client_GetInstallImageState_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *Client_GetInstallImageState_Call) RunAndReturn(run func() (sshclient.InstallImageState, error)) *Client_GetInstallImageState_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// GetResultOfInstallImage provides a mock function with given fields:
+func (_m *Client) GetResultOfInstallImage() (string, error) {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetResultOfInstallImage")
+	}
+
+	var r0 string
+	var r1 error
+	if rf, ok := ret.Get(0).(func() (string, error)); ok {
+		return rf()
+	}
+	if rf, ok := ret.Get(0).(func() string); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	if rf, ok := ret.Get(1).(func() error); ok {
+		r1 = rf()
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Client_GetResultOfInstallImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetResultOfInstallImage'
+type Client_GetResultOfInstallImage_Call struct {
+	*mock.Call
+}
+
+// GetResultOfInstallImage is a helper method to define mock.On call
+func (_e *Client_Expecter) GetResultOfInstallImage() *Client_GetResultOfInstallImage_Call {
+	return &Client_GetResultOfInstallImage_Call{Call: _e.mock.On("GetResultOfInstallImage")}
+}
+
+func (_c *Client_GetResultOfInstallImage_Call) Run(run func()) *Client_GetResultOfInstallImage_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *Client_GetResultOfInstallImage_Call) Return(_a0 string, _a1 error) *Client_GetResultOfInstallImage_Call {
+	_c.Call.Return(_a0, _a1)
 	return _c
 }
 
-func (_c *Client_GetRunningInstallImageProcesses_Call) RunAndReturn(run func() sshclient.Output) *Client_GetRunningInstallImageProcesses_Call {
+func (_c *Client_GetResultOfInstallImage_Call) RunAndReturn(run func() (string, error)) *Client_GetResultOfInstallImage_Call {
 	_c.Call.Return(run)
 	return _c
 }
diff --git a/pkg/services/baremetal/client/ssh/ssh_client.go b/pkg/services/baremetal/client/ssh/ssh_client.go
index c9345e0a..4cca01dc 100644
--- a/pkg/services/baremetal/client/ssh/ssh_client.go
+++ b/pkg/services/baremetal/client/ssh/ssh_client.go
@@ -176,6 +176,55 @@ type Output struct {
 	Err    error
 }
 
+// InstallImageState defines three states of the process.
+type InstallImageState string
+
+const (
+	// InstallImageStateNotStartedYet means the process has not started yet.
+	InstallImageStateNotStartedYet InstallImageState = "not-started-yet"
+	// InstallImageStateRunning means the process is still running.
+	InstallImageStateRunning InstallImageState = "running"
+	// InstallImageStateFinished has finished.
+	InstallImageStateFinished InstallImageState = "finished"
+)
+
+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, ". ")
+}
+
+// ExitStatus returns the exit status of the remote shell command.
+// There are three case:
+// First case: Remote command finished with exit 0: 0, nil.
+// Second case: Remote command finished with non zero: N, nil.
+// Third case: Remote command was not called successfully (like host not reachable): 0, err.
+func (o Output) ExitStatus() (int, error) {
+	var exitError *ssh.ExitError
+	if errors.As(o.Err, &exitError) {
+		return exitError.ExitStatus(), nil
+	}
+	return 0, o.Err
+}
+
 // Client is the interface defining all functions necessary to talk to a bare metal server via SSH.
 type Client interface {
 	GetHostName() Output
@@ -189,17 +238,14 @@ type Client interface {
 	GetHardwareDetailsCPUThreads() Output
 	GetHardwareDetailsCPUCores() Output
 	GetHardwareDetailsDebug() Output
-	GetRunningInstallImageProcesses() Output
+	GetInstallImageState() (InstallImageState, error)
+	GetResultOfInstallImage() (string, error)
 	GetCloudInitOutput() Output
 	CreateAutoSetup(data string) Output
 	DownloadImage(path, url string) Output
 	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 +372,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,12 +408,29 @@ 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.
-func (c *sshClient) GetRunningInstallImageProcesses() Output {
-	return c.runSSH(`ps aux| grep installimage | grep -v grep; true`)
+// GetInstallImageState returns the running installimage processes.
+func (c *sshClient) GetInstallImageState() (InstallImageState, error) {
+	out := c.runSSH(`ps aux| grep installimage | grep -v grep; true`)
+	if out.Err != nil {
+		return "", fmt.Errorf("failed to run `ps aux` to get running installimage process: %w", out.Err)
+	}
+	if out.StdOut != "" {
+		// installimage is running
+		return InstallImageStateRunning, nil
+	}
+
+	out = c.runSSH(`[ -e /root/installimage-wrapper.sh.log ]`)
+	exists, err := out.ExitStatus()
+	if err != nil {
+		return "", fmt.Errorf("failed to check if installimage-wrapper.sh.log exists: %w", err)
+	}
+	if exists == 0 {
+		return InstallImageStateFinished, nil
+	}
+	return InstallImageStateNotStartedYet, nil
 }
 
 // ExecuteInstallImage implements the ExecuteInstallImage method of the SSHClient interface.
@@ -395,32 +458,34 @@ EOF_VIA_SSH`, cmd))
 		return out
 	}
 
-	installImageOut := c.runSSH(`sh /root/installimage-wrapper.sh`)
+	return c.runSSH(`nohup /root/installimage-wrapper.sh >/root/installimage-wrapper.sh.log 2>&1 </dev/null &`)
+}
 
-	debugTxtOut := c.runSSH(`cat /root/debug.txt`)
-	installImageOut.StdOut = fmt.Sprintf(`debug.txt:
-%s
-%s
-######################################
+// GetResultOfInstallImage returns the logs of install-image.
+// Before calling this method be sure that installimage is already terminated.
+func (c *sshClient) GetResultOfInstallImage() (string, error) {
+	out := c.runSSH(`cat /root/debug.txt`)
+	if out.Err != nil {
+		return "", fmt.Errorf("failed to get debug.txt: %w", out.Err)
+	}
+	debugTxt := out.StdOut
 
-/root/installimage-wrapper.sh stdout:
-%s
-######################################
+	out = c.runSSH(`cat /root/installimage-wrapper.sh.log`)
+	if out.Err != nil {
+		return "", fmt.Errorf("failed to get installimage-wrapper.sh.log: %w", out.Err)
+	}
+	wrapperLog := out.StdOut
 
-stderr:
-%s`,
-		debugTxtOut.StdOut, debugTxtOut.StdErr, installImageOut.StdOut, installImageOut.StdErr)
+	return fmt.Sprintf(`debug.txt:
+%s
 
-	if installImageOut.Err != nil {
-		return installImageOut
-	}
+######################################
 
-	if debugTxtOut.Err != nil {
-		installImageOut.StdOut += fmt.Sprintf("\nfailed to get /root/debug.txt:\n%s", debugTxtOut.Err.Error())
-	}
+/root/installimage-wrapper.sh stdout+stderr:
 
-	// Ignore StdErr in this command
-	return Output{StdOut: installImageOut.StdOut}
+%s
+`,
+		debugTxt, wrapperLog), nil
 }
 
 // Reboot implements the Reboot method of the SSHClient interface.
@@ -432,30 +497,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")
@@ -512,12 +553,14 @@ EOF_VIA_SSH
 }
 
 func (c *sshClient) UntarTGZ() Output {
+	// read tgz from container image.
 	fileName := "/installimage.tgz"
 	data, err := os.ReadFile(fileName)
 	if err != nil {
 		return Output{Err: fmt.Errorf("ReadInstallimageTgzFailed %s: %w", fileName, err)}
 	}
 
+	// send base64 encoded binary to machine via ssh.
 	return c.runSSH(fmt.Sprintf("echo %s | base64 -d | tar -xzf-",
 		base64.StdEncoding.EncodeToString(data)))
 }
diff --git a/pkg/services/baremetal/host/host.go b/pkg/services/baremetal/host/host.go
index cafba0fa..d31b20b6 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)
@@ -1064,7 +1074,27 @@ func (s *Service) actionImageInstalling() actionResult {
 	}
 
 	s.scope.HetznerBareMetalHost.Spec.Status.SSHStatus.OSKey = &sshKey
+	state, err := sshClient.GetInstallImageState()
+	if err != nil {
+		return actionError{err: fmt.Errorf("failed to get state of installimage processes: %w", err)}
+	}
+	switch state {
+	case sshclient.InstallImageStateRunning:
+		s.scope.Logger.Info("installimage is still running. Checking again in some seconds.")
+		return actionContinue{delay: 10 * time.Second}
+
+	case sshclient.InstallImageStateFinished:
+		s.scope.Logger.Info("installimage is finished.")
+		return s.actionImageInstallingFinished(ctx, sshClient)
+	case sshclient.InstallImageStateNotStartedYet:
+		// install-image not started yet. Start it now.
+		return s.actionImageInstallingStartBackgroundProcess(ctx, sshClient)
+	default:
+		panic(fmt.Sprintf("Unknown InstallImageState %+v", state))
+	}
+}
 
+func (s *Service) actionImageInstallingStartBackgroundProcess(ctx context.Context, sshClient sshclient.Client) actionResult {
 	// If there is a Linux OS on an other disk, then the reboot after the provisioning
 	// will likely fail, because the machine boots into the other operating system.
 	// We want detect that early, and not start the provisioning process.
@@ -1105,20 +1135,6 @@ func (s *Service) actionImageInstalling() actionResult {
 	}
 	record.Eventf(s.scope.HetznerBareMetalHost, "NoLinuxOnAnotherDisk", "OK, no Linux on another disk:\n%s\n\n%s", out.StdOut, out.StdErr)
 
-	// if the previous reconcile was stopped, then wait until the first
-	// run of installimage was finished.
-	out = sshClient.GetRunningInstallImageProcesses()
-	if out.Err != nil {
-		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,
-		}
-	}
-
 	record.Event(s.scope.HetznerBareMetalHost, "InstallImagePreflightCheckSuccessful", "Rescue system reachable, disks look good.")
 
 	autoSetupInput, actionRes := s.createAutoSetupInput(sshClient)
@@ -1140,10 +1156,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,31 +1204,48 @@ 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)}
 	}
+	return actionContinue{delay: 10 * time.Second}
+}
+
+func (s *Service) actionImageInstallingFinished(_ context.Context, sshClient sshclient.Client) actionResult {
+	output, err := sshClient.GetResultOfInstallImage()
+	if err != nil {
+		return actionError{
+			err: fmt.Errorf("GetResultOfInstallImage failed: %w", err),
+		}
+	}
+	if !strings.Contains(output, PostInstallScriptFinished) {
+		record.Warn(s.scope.HetznerBareMetalHost, "InstallImageNotSuccessful", output)
+		return actionError{err: fmt.Errorf("did not find marker %q in stdout. Installimage was not successful: %s",
+			PostInstallScriptFinished, output)}
+	}
 
-	record.Eventf(s.scope.HetznerBareMetalHost, "ExecuteInstallImageSucceeded", out.StdOut)
-	s.scope.Logger.Info("ExecuteInstallImageSucceeded", "stdout", out.StdOut, "stderr", out.StdErr)
+	record.Event(s.scope.HetznerBareMetalHost, "InstallImageOutput", output)
+	s.scope.Logger.Info("InstallImageOutput", "output", output)
 
 	// Update name in robot API
-	if _, err := s.scope.RobotClient.SetBMServerName(s.scope.HetznerBareMetalHost.Spec.ServerID, autoSetupInput.hostName); err != nil {
+	if _, err := s.scope.RobotClient.SetBMServerName(s.scope.HetznerBareMetalHost.Spec.ServerID, s.scope.Hostname()); err != nil {
 		record.Warn(s.scope.HetznerBareMetalHost, "SetBMServerNameFailed", err.Error())
 		s.handleRobotRateLimitExceeded(err, "SetBMServerName")
 		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 +1316,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.PermanentError, 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 +1379,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,
@@ -1449,42 +1387,15 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
 		IP:         s.scope.HetznerBareMetalHost.Spec.Status.GetIPAddress(),
 	})
 
-	defer func() {
-		// Create an Event which contains the content of /var/log/cloud-init-output.log
-
-		if _, ok := ar.(actionContinue); ok {
-			// don't create an event
-			return
-		}
-		out := sshClient.GetCloudInitOutput()
-		if out.Err != nil || out.StdErr != "" {
-			record.Warnf(s.scope.HetznerBareMetalHost, "GetCloudInitOutputFailed",
-				fmt.Sprintf("GetCloudInitOutput failed to get /var/log/cloud-init-output.log: stdout %q, stderr %q, err %q",
-					out.StdOut, out.StdErr, out.Err.Error()))
-			return
-		}
-		_, ok := ar.(actionComplete)
-		if ok {
-			record.Eventf(s.scope.HetznerBareMetalHost, "CloudInitOutput",
-				"/var/log/cloud-init-output.log: "+out.StdOut)
-		} else {
-			_, err := ar.Result()
-			errString := ""
-			if err != nil {
-				errString = err.Error()
-			}
-			record.Warnf(s.scope.HetznerBareMetalHost, "CloudInitOutput", fmt.Sprintf("cloud init output (%s):\n%s",
-				errString,
-				out.StdOut))
-		}
-	}()
 	// Check hostname with sshClient
 	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,13 +1411,14 @@ 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
 			}
 		}
 
 		failed, err := s.handleIncompleteBoot(false, isTimeout, isSSHConnectionRefusedError)
 		if failed {
-			return s.recordActionFailure(infrav1.PermanentError, err.Error())
+			return s.recordActionFailure(infrav1.ProvisioningError, err.Error())
 		}
 		if err != nil {
 			return actionError{err: fmt.Errorf(errMsgFailedHandlingIncompleteBoot, err)}
@@ -1514,10 +1426,45 @@ func (s *Service) actionEnsureProvisioned() (ar actionResult) {
 		return actionContinue{delay: 10 * time.Second}
 	}
 
+	// from now on we know that the machine is reachable and
+	// is no longer in the rescue system.
+
+	createEventWithCloudInitOutput := func(ar actionResult) actionResult {
+		// Create an Event which contains the cloud-init-output.
+		var err error
+		errMsg := ""
+		f := record.Warnf
+		switch v := ar.(type) {
+		case actionContinue:
+			// Do not create and event containing the output, wait until finished.
+			return ar
+		case actionComplete:
+			f = record.Eventf
+		case actionError:
+			err = v.err
+			errMsg = fmt.Sprintf(" (%s)", v.err.Error())
+		}
+		out := sshClient.GetCloudInitOutput()
+		if out.Err != nil || out.StdErr != "" {
+			record.Warnf(s.scope.HetznerBareMetalHost, "GetCloudInitOutputFailed",
+				"GetCloudInitOutput failed to get /var/log/cloud-init-output.log: stdout %q, stderr %q, err %q",
+				out.StdOut, out.StdErr, out.Err.Error())
+			if err != nil {
+				return actionError{err: fmt.Errorf("failed to get cloud init output: %w, while handling: %w", out.Err, err)}
+			}
+			return actionError{err: fmt.Errorf("failed to get cloud init output: %w", err)}
+		}
+		f(s.scope.HetznerBareMetalHost, "CloudInitOutput", "cloud init output%s:\n%s",
+			errMsg,
+			out.StdOut)
+		return ar
+	}
+
 	// Check the status of cloud init
-	actResult, _ := s.checkCloudInitStatus(sshClient)
+	actResult, msg, _ := s.checkCloudInitStatus(sshClient)
 	if _, complete := actResult.(actionComplete); !complete {
-		return actResult
+		record.Event(s.scope.HetznerBareMetalHost, "CloudInitStillRunning", msg)
+		return createEventWithCloudInitOutput(actResult)
 	}
 
 	// Check whether cloud init did not run successfully even though it shows "done"
@@ -1526,14 +1473,15 @@ 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 {
-			return actResult
+			s.scope.Logger.Info("ensureProvisioned: handleCloudInitNotStarted", "actResult", actResult)
+			return createEventWithCloudInitOutput(actResult)
 		}
 	}
 
 	record.Event(s.scope.HetznerBareMetalHost, "ServerProvisioned", "server successfully provisioned")
 	conditions.MarkTrue(s.scope.HetznerBareMetalHost, infrav1.ProvisionSucceededCondition)
 	s.scope.HetznerBareMetalHost.ClearError()
-	return actionComplete{}
+	return createEventWithCloudInitOutput(actionComplete{}) // next: actionProvisioned
 }
 
 // handleConnectionRefused checks cloud init status via ssh to the old ssh port if the new ssh port
@@ -1548,7 +1496,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 +1517,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 +1579,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 +1626,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 +1683,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 +1726,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/pkg/services/baremetal/host/utils.go b/pkg/services/baremetal/host/utils.go
index 9e0edada..172b7b39 100644
--- a/pkg/services/baremetal/host/utils.go
+++ b/pkg/services/baremetal/host/utils.go
@@ -29,9 +29,9 @@ type autoSetupInput struct {
 	image     string
 }
 
-func buildAutoSetup(installImageSpec *infrav1.InstallImage, autoSetupInput autoSetupInput) string {
+func buildAutoSetup(installImageSpec *infrav1.InstallImage, asi autoSetupInput) string {
 	var drives string
-	for i, osDevice := range autoSetupInput.osDevices {
+	for i, osDevice := range asi.osDevices {
 		if i > 0 {
 			drives = fmt.Sprintf(`%s
 DRIVE%v /dev/%s`, drives, i+1, osDevice)
@@ -42,7 +42,7 @@ DRIVE%v /dev/%s`, drives, i+1, osDevice)
 
 	hostName := fmt.Sprintf(`
 HOSTNAME %s
-SWRAID %v`, autoSetupInput.hostName, installImageSpec.Swraid)
+SWRAID %v`, asi.hostName, installImageSpec.Swraid)
 	if installImageSpec.Swraid == 1 {
 		hostName = fmt.Sprintf(`%s
 SWRAIDLEVEL %v`, hostName, installImageSpec.SwraidLevel)
@@ -70,7 +70,7 @@ SUBVOL %s %s %s`, btrfsDefinitions, btrfs.Volume, btrfs.SubVolume, btrfs.Mount)
 	}
 
 	image := fmt.Sprintf(`
-IMAGE %s`, autoSetupInput.image)
+IMAGE %s`, asi.image)
 
 	output := fmt.Sprintf(`%s
 %s
diff --git a/pkg/services/hcloud/server/server.go b/pkg/services/hcloud/server/server.go
index c42fe286..10cf0aad 100644
--- a/pkg/services/hcloud/server/server.go
+++ b/pkg/services/hcloud/server/server.go
@@ -459,14 +459,34 @@ func (s *Service) createServer(ctx context.Context) (*hcloud.Server, error) {
 				clusterv1.ConditionSeverityError,
 				err.Error(),
 			)
-			record.Warnf(s.scope.HCloudMachine,
-				"FailedCreateHCloudServer",
-				"Failed to create HCloud server %s: %s",
-				s.scope.Name(),
-				err,
+
+			err = errors.Join(errServerCreateNotPossible, err)
+		}
+
+		if hcloud.IsError(err, hcloud.ErrorCodeResourceUnavailable) {
+			conditions.MarkFalse(
+				s.scope.HCloudMachine,
+				infrav1.ServerCreateSucceededCondition,
+				infrav1.ServerResourceUnavailableReason,
+				clusterv1.ConditionSeverityWarning,
+				err.Error(),
 			)
-			return nil, errServerCreateNotPossible
+
+			err = errors.Join(errServerCreateNotPossible, err)
 		}
+
+		if hcloud.IsError(err, hcloud.ErrorCodePlacementError) {
+			conditions.MarkFalse(
+				s.scope.HCloudMachine,
+				infrav1.ServerCreateSucceededCondition,
+				infrav1.ServerPlacementErrorReason,
+				clusterv1.ConditionSeverityWarning,
+				err.Error(),
+			)
+
+			err = errors.Join(errServerCreateNotPossible, err)
+		}
+
 		record.Warnf(s.scope.HCloudMachine,
 			"FailedCreateHCloudServer",
 			"Failed to create HCloud server %s: %s",
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..4542cfee 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 (flaky)", 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/hcloudmachinetemplate_controller_test.go b/controllers/hcloudmachinetemplate_controller_test.go
index af3bd04a..bccbedd8 100644
--- a/controllers/hcloudmachinetemplate_controller_test.go
+++ b/controllers/hcloudmachinetemplate_controller_test.go
@@ -54,9 +54,7 @@ var _ = Describe("HCloudMachineTemplateReconciler", func() {
 
 	Context("Basic test", func() {
 		Context("ClusterClass test", func() {
-			var (
-				capiClusterClass *clusterv1.ClusterClass
-			)
+			var capiClusterClass *clusterv1.ClusterClass
 
 			BeforeEach(func() {
 				capiClusterClass = &clusterv1.ClusterClass{
@@ -292,7 +290,6 @@ var _ = Describe("HCloudMachineTemplateReconciler", func() {
 
 				hcloudMachineTemplate.Spec.Template.Spec.ImageName = "fedora-control-plane"
 				Expect(testEnv.Client.Update(ctx, hcloudMachineTemplate)).ToNot(Succeed())
-
 			})
 
 			It("should prevent updating SSHKey", func() {
@@ -300,7 +297,6 @@ var _ = Describe("HCloudMachineTemplateReconciler", func() {
 
 				hcloudMachineTemplate.Spec.Template.Spec.SSHKeys = []infrav1.SSHKey{{Name: "ssh-key-1"}}
 				Expect(testEnv.Client.Update(ctx, hcloudMachineTemplate)).ToNot(Succeed())
-
 			})
 
 			It("should prevent updating PlacementGroups", func() {
@@ -308,7 +304,6 @@ var _ = Describe("HCloudMachineTemplateReconciler", func() {
 
 				hcloudMachineTemplate.Spec.Template.Spec.PlacementGroupName = createPlacementGroupName("placement-group-1")
 				Expect(testEnv.Client.Update(ctx, hcloudMachineTemplate)).ToNot(Succeed())
-
 			})
 
 			It("should succeed for mutable fields", func() {
@@ -325,7 +320,6 @@ var _ = Describe("HCloudMachineTemplateReconciler", func() {
 				Expect(testEnv.Client.Update(ctx, hcloudMachineTemplate)).To(Succeed())
 			})
 		})
-
 	})
 })
 
diff --git a/controllers/hetznerbaremetalhost_controller_test.go b/controllers/hetznerbaremetalhost_controller_test.go
index fc244c75..2d9e897b 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,9 +892,10 @@ 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{})
-	sshClient.On("GetRunningInstallImageProcesses").Return(sshclient.Output{})
+	sshClient.On("GetInstallImageState").Return(sshclient.InstallImageStateFinished, nil)
+	sshClient.On("GetResultOfInstallImage").Return(hostpkg.PostInstallScriptFinished, nil)
 }
diff --git a/controllers/hetznerbaremetalmachine_controller_test.go b/controllers/hetznerbaremetalmachine_controller_test.go
index 852c1621..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{})
@@ -709,44 +705,6 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
 				Expect(testEnv.Update(ctx, bmMachine)).NotTo(Succeed())
 			})
 		})
-
-		Context("validate create", func() {
-			var (
-				hbmmt  *infrav1.HetznerBareMetalMachineTemplate
-				testNs *corev1.Namespace
-			)
-			BeforeEach(func() {
-				var err error
-				testNs, err = testEnv.CreateNamespace(ctx, "hcloudmachine-validation")
-				Expect(err).NotTo(HaveOccurred())
-
-				hbmmt = &infrav1.HetznerBareMetalMachineTemplate{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      bmMachineName,
-						Namespace: testNs.Name,
-						Labels: map[string]string{
-							clusterv1.ClusterNameLabel: capiCluster.Name,
-						},
-					},
-					Spec: infrav1.HetznerBareMetalMachineTemplateSpec{
-						Template: infrav1.HetznerBareMetalMachineTemplateResource{
-							Spec: getDefaultHetznerBareMetalMachineSpec(),
-						},
-					},
-				}
-
-			})
-
-			AfterEach(func() {
-				Expect(testEnv.Cleanup(ctx, testNs, hbmmt)).To(Succeed())
-			})
-
-			It("should allow creation of hbmmt", func() {
-				Expect(testEnv.Create(ctx, hbmmt)).To(Succeed())
-			})
-
-		})
-
 		Context("validate update", func() {
 			var (
 				hbmmt  *infrav1.HetznerBareMetalMachineTemplate
@@ -777,7 +735,6 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
 				Eventually(func() error {
 					return testEnv.Client.Get(ctx, key, hbmmt)
 				}, timeout, time.Second).Should(BeNil())
-
 			})
 
 			AfterEach(func() {
@@ -807,7 +764,6 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
 					},
 				}
 				Expect(testEnv.Client.Update(ctx, hbmmt)).ToNot(Succeed())
-
 			})
 
 			It("should not allow update of SSHSpec", func() {
@@ -832,7 +788,6 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
 					},
 				}
 				Expect(testEnv.Client.Update(ctx, hbmmt)).ToNot(Succeed())
-
 			})
 
 			It("should not allow update of Host Selectors", func() {
@@ -857,7 +812,6 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
 					},
 				}
 				Expect(testEnv.Client.Update(ctx, hbmmt)).ToNot(Succeed())
-
 			})
 
 			It("should allow update of mutable fields", func() {
@@ -868,9 +822,7 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
 				}
 				hbmmt.ObjectMeta.Annotations["test"] = "should_succeed"
 				Expect(testEnv.Client.Update(ctx, hbmmt)).To(Succeed())
-
 			})
-
 		})
 	})
 })
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/pkg/services/baremetal/host/utils_test.go b/pkg/services/baremetal/host/utils_test.go
index 0065594e..acbf9572 100644
--- a/pkg/services/baremetal/host/utils_test.go
+++ b/pkg/services/baremetal/host/utils_test.go
@@ -26,12 +26,12 @@ import (
 var _ = Describe("buildAutoSetup", func() {
 	type testCaseBuildAutoSetup struct {
 		installImageSpec *infrav1.InstallImage
-		autoSetupInput   autoSetupInput
+		asi              autoSetupInput
 		expectedOutput   string
 	}
 	DescribeTable("buildAutoSetup",
 		func(tc testCaseBuildAutoSetup) {
-			Expect(buildAutoSetup(tc.installImageSpec, tc.autoSetupInput)).Should(Equal(tc.expectedOutput))
+			Expect(buildAutoSetup(tc.installImageSpec, tc.asi)).Should(Equal(tc.expectedOutput))
 		},
 		Entry("multiple entries", testCaseBuildAutoSetup{
 			installImageSpec: &infrav1.InstallImage{
@@ -78,7 +78,7 @@ var _ = Describe("buildAutoSetup", func() {
 				Swraid:      0,
 				SwraidLevel: 1,
 			},
-			autoSetupInput: autoSetupInput{
+			asi: autoSetupInput{
 				image:     "my-image",
 				osDevices: []string{"device"},
 				hostName:  "my-host",
@@ -127,7 +127,7 @@ IMAGE my-image`,
 				Swraid:      1,
 				SwraidLevel: 1,
 			},
-			autoSetupInput: autoSetupInput{
+			asi: autoSetupInput{
 				image:     "my-image",
 				osDevices: []string{"device"},
 				hostName:  "my-host",
@@ -174,7 +174,7 @@ IMAGE my-image`,
 				Swraid:      0,
 				SwraidLevel: 1,
 			},
-			autoSetupInput: autoSetupInput{
+			asi: autoSetupInput{
 				image:     "my-image",
 				osDevices: []string{"device1", "device2"},
 				hostName:  "my-host",
@@ -207,7 +207,7 @@ IMAGE my-image`,
 				Swraid:           0,
 				SwraidLevel:      1,
 			},
-			autoSetupInput: autoSetupInput{
+			asi: autoSetupInput{
 				image:     "my-image",
 				osDevices: []string{"device"},
 				hostName:  "my-host",
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..0cf044cc 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"
+    wwn: "0x5000cca22de4ef84"
   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