Repository: syself/cluster-api-provider-hetzner
Pull request #1453 (2024-08-19 11:02:06)

:seedling: Update Caph Image group to v
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| docker.io/library/golang | stage | digest | `825f815` -> `0838740` |

---

### Configuration

📅 **Schedule**: Branch creation - "on monday" in timezone Europe/Berlin, 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:eyJjcmVhdGVkSW5WZXIiOiIzOC40MC4xIiwidXBkYXRlZEluVmVyIjoiMzguNDAuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsidXBkYXRlL2NvbnRhaW5lciJdfQ==-->



Patch:
diff --git a/.github/actions/manager-image/action.yaml b/.github/actions/manager-image/action.yaml
index 838ffc9c..65f51475 100644
--- a/.github/actions/manager-image/action.yaml
+++ b/.github/actions/manager-image/action.yaml
@@ -44,7 +44,7 @@ runs:
 
     # Import GitHub's cache build to docker cache
     - name: Copy Caph Golang cache to docker cache
-      uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1
+      uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
       with:
         provenance: false
         context: /tmp/.cache/caph
@@ -54,7 +54,7 @@ runs:
         target: import-cache
 
     - name: Build and push manager image
-      uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6
+      uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6
       with:
         provenance: false
         context: .
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 745ec36f..661b93d6 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -88,7 +88,7 @@ jobs:
 
       # Import GitHub's cache build to docker cache
       - name: Copy Caph Golang cache to docker cache
-        uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1
+        uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
         with:
           provenance: false
           context: /tmp/.cache/caph
@@ -98,7 +98,7 @@ jobs:
           target: import-cache
 
       - name: Build and push manager image
-        uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6
+        uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6
         id: docker_build_release
         with:
           provenance: false
@@ -150,7 +150,7 @@ jobs:
       # Store docker's golang's cache build locally only on the main branch
       - name: Store Caph Golang cache build locally
         if: ${{ steps.cache.outputs.cache-hit != 'true' }}
-        uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1
+        uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
         with:
           provenance: false
           context: .
diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml
index f5be1421..a9f6bfdd 100644
--- a/.github/workflows/pr-verify.yml
+++ b/.github/workflows/pr-verify.yml
@@ -48,7 +48,7 @@ jobs:
           app-id: ${{ secrets.SYSELF_APP_ID }}
           private-key: ${{ secrets.SYSELF_APP_PRIVATE_KEY }}
       - name: Generate Size
-        uses: pascalgn/size-label-action@bbbaa0d5ccce8e2e76254560df5c64b82dac2e12 # v0.5.2
+        uses: pascalgn/size-label-action@be08a2d5f857dc99c5b9426cdb5a8ea1aa7f0399 # v0.5.4
         env:
           GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
         with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7cc1d9d7..6c4a51fa 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -60,7 +60,7 @@ jobs:
           echo 'EOF' >> $GITHUB_ENV
 
       - name: Build and push manager image
-        uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6
+        uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6
         id: docker_build_release
         with:
           provenance: false
diff --git a/.github/workflows/schedule-update-bot.yaml b/.github/workflows/schedule-update-bot.yaml
index 52f7fccb..37165b84 100644
--- a/.github/workflows/schedule-update-bot.yaml
+++ b/.github/workflows/schedule-update-bot.yaml
@@ -44,7 +44,7 @@ jobs:
           echo "LOG_LEVEL=${{ github.event.inputs.logLevel || env.LOG_LEVEL }}" >> "$GITHUB_ENV"
 
       - name: Renovate
-        uses: renovatebot/github-action@0c94129d48b4e92addf23686b8a735b3057a0448 # v40.2.5
+        uses: renovatebot/github-action@630a255a1f2f56c8d8ce160bed3e3ca577ca53e2 # v40.2.7
         env:
           RENOVATE_HOST_RULES: '[{"hostType": "docker", "matchHost": "ghcr.io", "username": "${{ github.actor }}", "password": "${{ secrets.GITHUB_TOKEN }}" }]'
           RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '[".*"]'
diff --git a/api/v1beta1/hcloudmachine_types.go b/api/v1beta1/hcloudmachine_types.go
index 19f5c489..8825f48e 100644
--- a/api/v1beta1/hcloudmachine_types.go
+++ b/api/v1beta1/hcloudmachine_types.go
@@ -24,10 +24,13 @@ import (
 )
 
 const (
-	// MachineFinalizer allows ReconcileHCloudMachine to clean up HCloud
+	// HCloudMachineFinalizer allows ReconcileHCloudMachine to clean up HCloud
 	// resources associated with HCloudMachine before removing it from the
 	// apiserver.
-	MachineFinalizer = "hcloudmachine.infrastructure.cluster.x-k8s.io"
+	HCloudMachineFinalizer = "infrastructure.cluster.x-k8s.io/hcloudmachine"
+	// DeprecatedHCloudMachineFinalizer contains the old string.
+	// The controller will automatically update to the new string.
+	DeprecatedHCloudMachineFinalizer = "hcloudmachine.infrastructure.cluster.x-k8s.io"
 )
 
 // HCloudMachineSpec defines the desired state of HCloudMachine.
diff --git a/api/v1beta1/hcloudremediation_types.go b/api/v1beta1/hcloudremediation_types.go
index e77fc819..695b1451 100644
--- a/api/v1beta1/hcloudremediation_types.go
+++ b/api/v1beta1/hcloudremediation_types.go
@@ -21,15 +21,6 @@ import (
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 )
 
-const (
-	// HCloudRemediationFinalizer allows HCloudRemediationReconciler to clean up resources associated with HCloudRemediation before
-	// removing it from the apiserver.
-	HCloudRemediationFinalizer = "hcloudremediation.infrastructure.cluster.x-k8s.io"
-
-	// HCloudRebootAnnotation indicates that a bare metal host object should be rebooted.
-	HCloudRebootAnnotation = "reboot.hcloud.infrastructure.cluster.x-k8s.io"
-)
-
 // HCloudRemediationSpec defines the desired state of HCloudRemediation.
 type HCloudRemediationSpec struct {
 	// Strategy field defines remediation strategy.
diff --git a/api/v1beta1/hetznerbaremetalhost_types.go b/api/v1beta1/hetznerbaremetalhost_types.go
index 51456cbd..496f3fff 100644
--- a/api/v1beta1/hetznerbaremetalhost_types.go
+++ b/api/v1beta1/hetznerbaremetalhost_types.go
@@ -21,7 +21,6 @@ import (
 	"crypto/sha256"
 	"encoding/json"
 	"fmt"
-	"strings"
 	"time"
 
 	corev1 "k8s.io/api/core/v1"
@@ -31,10 +30,14 @@ import (
 )
 
 const (
-	// BareMetalHostFinalizer is the name of the finalizer added to
+	// HetznerBareMetalHostFinalizer is the name of the finalizer added to
 	// hosts to block delete operations until the physical host can be
 	// deprovisioned.
-	BareMetalHostFinalizer string = "hetznerbaremetalhost.infrastructure.cluster.x-k8s.io"
+	HetznerBareMetalHostFinalizer = "infrastructure.cluster.x-k8s.io/hetznerbaremetalhost"
+
+	// DeprecatedBareMetalHostFinalizer contains the old string.
+	// The controller will automatically update to the new string.
+	DeprecatedBareMetalHostFinalizer = "hetznerbaremetalhost.infrastructure.cluster.x-k8s.io"
 
 	// HostAnnotation is the key for an annotation that should go on a HetznerBareMetalMachine to
 	// reference what HetznerBareMetalHost it corresponds to.
@@ -42,10 +45,10 @@ const (
 
 	// WipeDiskAnnotation indicates which Disks (WWNs) to erase before provisioning
 	// The value is a list of WWNS or "all".
-	WipeDiskAnnotation = "wipedisk.hetznerbaremetalhost.infrastructure.cluster.x-k8s.io"
+	WipeDiskAnnotation = "capi.syself.com/wipe-disk"
 
 	// IgnoreCheckDiskAnnotation indicates that the machine should get provisioned, even if CheckDisk fails.
-	IgnoreCheckDiskAnnotation = "ignore-cd.hetznerbaremetalhost.infrastructure.cluster.x-k8s.io" // max length is 63 chars.
+	IgnoreCheckDiskAnnotation = "capi.syself.com/ignore-check-disk"
 )
 
 // RootDeviceHints holds the hints for specifying the storage location
@@ -132,17 +135,17 @@ const (
 
 const (
 	// ErrorMessageMissingRootDeviceHints specifies the error message when no root device hints are specified.
-	ErrorMessageMissingRootDeviceHints string = "no root device hints specified"
+	ErrorMessageMissingRootDeviceHints = "no root device hints specified"
 	// ErrorMessageInvalidRootDeviceHints specifies the error message when invalid root device hints are specified.
-	ErrorMessageInvalidRootDeviceHints string = "invalid root device hints specified"
+	ErrorMessageInvalidRootDeviceHints = "invalid root device hints specified"
 	// ErrorMessageMissingHetznerSecret specifies the error message when no Hetzner secret is found.
-	ErrorMessageMissingHetznerSecret string = "could not find HetznerSecret"
+	ErrorMessageMissingHetznerSecret = "could not find HetznerSecret"
 	// ErrorMessageMissingRescueSSHSecret specifies the error message when no RescueSSH secret is found.
-	ErrorMessageMissingRescueSSHSecret string = "could not find RescueSSHSecret"
+	ErrorMessageMissingRescueSSHSecret = "could not find RescueSSHSecret"
 	// ErrorMessageMissingOSSSHSecret specifies the error message when no OSSSH secret is found.
-	ErrorMessageMissingOSSSHSecret string = "could not find OSSSHSecret"
+	ErrorMessageMissingOSSSHSecret = "could not find OSSSHSecret"
 	// ErrorMessageMissingOrInvalidSecretData specifies the error message when no data in secret is missing or invalid.
-	ErrorMessageMissingOrInvalidSecretData string = "invalid or not specified information in secret"
+	ErrorMessageMissingOrInvalidSecretData = "invalid or not specified information in secret"
 )
 
 // ProvisioningState defines the states of provisioning of the host.
@@ -598,7 +601,7 @@ func (host *HetznerBareMetalHost) ClearRebootAnnotations() {
 }
 
 func isRebootAnnotation(annotation string) bool {
-	return strings.HasPrefix(annotation, RebootAnnotation+"/") || annotation == RebootAnnotation
+	return annotation == RebootAnnotation
 }
 
 //+kubebuilder:object:root=true
diff --git a/api/v1beta1/hetznerbaremetalmachine_types.go b/api/v1beta1/hetznerbaremetalmachine_types.go
index 42b40760..66e5eff3 100644
--- a/api/v1beta1/hetznerbaremetalmachine_types.go
+++ b/api/v1beta1/hetznerbaremetalmachine_types.go
@@ -29,9 +29,13 @@ import (
 )
 
 const (
-	// BareMetalMachineFinalizer allows Reconcilehetznerbaremetalmachine to clean up resources associated with hetznerbaremetalmachine before
+	// HetznerBareMetalMachineFinalizer allows Reconcilehetznerbaremetalmachine to clean up resources associated with hetznerbaremetalmachine before
 	// removing it from the apiserver.
-	BareMetalMachineFinalizer = "hetznerbaremetalmachine.infrastructure.cluster.x-k8s.io"
+	HetznerBareMetalMachineFinalizer = "infrastructure.cluster.x-k8s.io/hetznerbaremetalmachine"
+
+	// DeprecatedBareMetalMachineFinalizer contains the old string.
+	// The controller will automatically update to the new string.
+	DeprecatedBareMetalMachineFinalizer = "hetznerbaremetalmachine.infrastructure.cluster.x-k8s.io"
 
 	// BareMetalHostNamePrefix is a prefix for all hostNames of bare metal servers.
 	BareMetalHostNamePrefix = "bm-"
diff --git a/api/v1beta1/hetznerbaremetalremediation_types.go b/api/v1beta1/hetznerbaremetalremediation_types.go
index a1829588..4dd47d88 100644
--- a/api/v1beta1/hetznerbaremetalremediation_types.go
+++ b/api/v1beta1/hetznerbaremetalremediation_types.go
@@ -21,17 +21,13 @@ import (
 )
 
 const (
-	// RemediationFinalizer allows HetznerBareMetalRemediationReconciler to clean up resources associated with HetznerBareMetalRemediation before
-	// removing it from the apiserver.
-	RemediationFinalizer = "hetznerbaremetalremediation.infrastructure.cluster.x-k8s.io"
-
 	// RebootAnnotation indicates that a bare metal host object should be rebooted.
-	RebootAnnotation = "reboot.hetznerbaremetalhost.infrastructure.cluster.x-k8s.io"
+	RebootAnnotation = "capi.syself.com/reboot"
 
 	// PermanentErrorAnnotation indicates that the bare metal host has an error which needs to be resolved manually.
 	// After the permanent error the annotation got removed (usually by a human), the controller removes
 	// ErrorType, ErrorCount and ErrorMessages, so that the hbmh will be usable again.
-	PermanentErrorAnnotation = "error.hetznerbaremetalhost.infrastructure.cluster.x-k8s.io"
+	PermanentErrorAnnotation = "capi.syself.com/permanent-error"
 )
 
 // HetznerBareMetalRemediationSpec defines the desired state of HetznerBareMetalRemediation.
diff --git a/api/v1beta1/hetznercluster_types.go b/api/v1beta1/hetznercluster_types.go
index fa99dc64..fbba1f28 100644
--- a/api/v1beta1/hetznercluster_types.go
+++ b/api/v1beta1/hetznercluster_types.go
@@ -22,10 +22,15 @@ import (
 )
 
 const (
-	// ClusterFinalizer allows ReconcileHetznerCluster to clean up HCloud
+	// HetznerClusterFinalizer allows ReconcileHetznerCluster to clean up HCloud
 	// resources associated with HetznerCluster before removing it from the
 	// apiserver.
-	ClusterFinalizer = "hetznercluster.infrastructure.cluster.x-k8s.io"
+	HetznerClusterFinalizer = "infrastructure.cluster.x-k8s.io/hetznercluster"
+
+	// DeprecatedHetznerClusterFinalizer contains the old string.
+	// The controller will automatically update to the new string.
+	DeprecatedHetznerClusterFinalizer = "hetznercluster.infrastructure.cluster.x-k8s.io"
+
 	// AllowEmptyControlPlaneAddressAnnotation allows HetznerCluster Webhook
 	// to skip some validation steps for externally managed control planes.
 	AllowEmptyControlPlaneAddressAnnotation = "capi.syself.com/allow-empty-control-plane-address"
diff --git a/api/v1beta1/hetznercluster_webhook.go b/api/v1beta1/hetznercluster_webhook.go
index 3904e459..72c0746e 100644
--- a/api/v1beta1/hetznercluster_webhook.go
+++ b/api/v1beta1/hetznercluster_webhook.go
@@ -74,10 +74,7 @@ func (r *HetznerCluster) ValidateCreate() (admission.Warnings, error) {
 	hetznerclusterlog.V(1).Info("validate create", "name", r.Name)
 	var allErrs field.ErrorList
 
-	allowEmptyControlPlaneAddress := false
-	if r.Annotations != nil {
-		allowEmptyControlPlaneAddress = r.Annotations[AllowEmptyControlPlaneAddressAnnotation] == "true"
-	}
+	allowEmptyControlPlaneAddress := r.Annotations[AllowEmptyControlPlaneAddressAnnotation] == "true"
 
 	if !allowEmptyControlPlaneAddress && len(r.Spec.ControlPlaneRegions) == 0 {
 		allErrs = append(allErrs, field.Invalid(
diff --git a/controllers/hcloudmachine_controller.go b/controllers/hcloudmachine_controller.go
index 221c8117..fe34ca57 100644
--- a/controllers/hcloudmachine_controller.go
+++ b/controllers/hcloudmachine_controller.go
@@ -187,7 +187,8 @@ func (r *HCloudMachineReconciler) reconcileDelete(ctx context.Context, machineSc
 		return result, nil
 	}
 	// Machine is deleted so remove the finalizer.
-	controllerutil.RemoveFinalizer(machineScope.HCloudMachine, infrav1.MachineFinalizer)
+	controllerutil.RemoveFinalizer(machineScope.HCloudMachine, infrav1.HCloudMachineFinalizer)
+	controllerutil.RemoveFinalizer(machineScope.HCloudMachine, infrav1.DeprecatedHCloudMachineFinalizer)
 
 	return reconcile.Result{}, nil
 }
@@ -196,7 +197,8 @@ func (r *HCloudMachineReconciler) reconcileNormal(ctx context.Context, machineSc
 	hcloudMachine := machineScope.HCloudMachine
 
 	// If the HCloudMachine doesn't have our finalizer, add it.
-	controllerutil.AddFinalizer(machineScope.HCloudMachine, infrav1.MachineFinalizer)
+	controllerutil.AddFinalizer(machineScope.HCloudMachine, infrav1.HCloudMachineFinalizer)
+	controllerutil.RemoveFinalizer(machineScope.HCloudMachine, infrav1.DeprecatedHCloudMachineFinalizer)
 
 	// Register the finalizer immediately to avoid orphaning HCloud resources on delete.
 	if err := machineScope.PatchObject(ctx); err != nil {
diff --git a/controllers/hcloudmachinetemplate_controller.go b/controllers/hcloudmachinetemplate_controller.go
index 16258da0..f0d370d0 100644
--- a/controllers/hcloudmachinetemplate_controller.go
+++ b/controllers/hcloudmachinetemplate_controller.go
@@ -78,7 +78,8 @@ func (r *HCloudMachineTemplateReconciler) Reconcile(ctx context.Context, req rec
 	}()
 
 	// removing finalizer that was set in previous versions but is not needed
-	controllerutil.RemoveFinalizer(machineTemplate, infrav1.MachineFinalizer)
+	// We can remove that code in 2025.
+	controllerutil.RemoveFinalizer(machineTemplate, infrav1.DeprecatedHCloudMachineFinalizer)
 
 	// Check whether owner is a ClusterClass. In that case there is nothing to do.
 	if hasOwnerClusterClass(machineTemplate.ObjectMeta) {
diff --git a/controllers/hetznerbaremetalhost_controller.go b/controllers/hetznerbaremetalhost_controller.go
index be069bc2..3b08a72e 100644
--- a/controllers/hetznerbaremetalhost_controller.go
+++ b/controllers/hetznerbaremetalhost_controller.go
@@ -35,6 +35,7 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -46,7 +47,6 @@ import (
 	robotclient "github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/client/robot"
 	sshclient "github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/client/ssh"
 	"github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/host"
-	"github.com/syself/cluster-api-provider-hetzner/pkg/utils"
 )
 
 // HetznerBareMetalHostReconciler reconciles a HetznerBareMetalHost object.
@@ -87,12 +87,12 @@ func (r *HetznerBareMetalHostReconciler) Reconcile(ctx context.Context, req ctrl
 	}
 
 	// Add a finalizer to newly created objects.
-	if bmHost.DeletionTimestamp.IsZero() && !hostHasFinalizer(bmHost) {
-		bmHost.Finalizers = append(bmHost.Finalizers,
-			infrav1.BareMetalHostFinalizer)
+	if bmHost.DeletionTimestamp.IsZero() &&
+		(controllerutil.AddFinalizer(bmHost, infrav1.HetznerBareMetalHostFinalizer) ||
+			controllerutil.RemoveFinalizer(bmHost, infrav1.DeprecatedBareMetalHostFinalizer)) {
 		err := r.Update(ctx, bmHost)
 		if err != nil {
-			return reconcile.Result{}, fmt.Errorf("failed to add finalizer: %w", err)
+			return reconcile.Result{}, fmt.Errorf("failed to update finalizer: %w", err)
 		}
 		return ctrl.Result{Requeue: true}, nil
 	}
@@ -230,7 +230,7 @@ func (r *HetznerBareMetalHostReconciler) reconcileSelectedStates(ctx context.Con
 		if needsUpdate {
 			err := r.Update(ctx, bmHost)
 			if err != nil {
-				return reconcile.Result{}, fmt.Errorf("failed to add finalizer: %w", err)
+				return reconcile.Result{}, fmt.Errorf("Update() failed after setting ProvisioningState: %w", err)
 			}
 		}
 
@@ -238,13 +238,12 @@ func (r *HetznerBareMetalHostReconciler) reconcileSelectedStates(ctx context.Con
 
 		// Handle StateDeleting
 	case infrav1.StateDeleting:
-		if !utils.StringInList(bmHost.Finalizers, infrav1.BareMetalHostFinalizer) {
-			return reconcile.Result{Requeue: true}, nil
-		}
-
-		bmHost.Finalizers = utils.FilterStringFromList(bmHost.Finalizers, infrav1.BareMetalHostFinalizer)
-		if err := r.Update(ctx, bmHost); err != nil {
-			return reconcile.Result{}, fmt.Errorf("failed to remove finalizer: %w", err)
+		if controllerutil.RemoveFinalizer(bmHost, infrav1.HetznerBareMetalHostFinalizer) ||
+			controllerutil.RemoveFinalizer(bmHost, infrav1.DeprecatedBareMetalHostFinalizer) {
+			// at least one finalizer was removed.
+			if err := r.Update(ctx, bmHost); err != nil {
+				return reconcile.Result{}, fmt.Errorf("failed to remove finalizer: %w", err)
+			}
 		}
 		return reconcile.Result{Requeue: true}, nil
 	}
@@ -411,10 +410,6 @@ func hetznerSecretErrorResult(
 	return reconcile.Result{}, fmt.Errorf("hetznerSecretErrorResult: an unhandled failure occurred: %T %w", err, err)
 }
 
-func hostHasFinalizer(host *infrav1.HetznerBareMetalHost) bool {
-	return utils.StringInList(host.Finalizers, infrav1.BareMetalHostFinalizer)
-}
-
 // SetupWithManager sets up the controller with the Manager.
 func (r *HetznerBareMetalHostReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
 	return ctrl.NewControllerManagedBy(mgr).
diff --git a/controllers/hetznerbaremetalmachine_controller.go b/controllers/hetznerbaremetalmachine_controller.go
index 92a96261..fa4de11f 100644
--- a/controllers/hetznerbaremetalmachine_controller.go
+++ b/controllers/hetznerbaremetalmachine_controller.go
@@ -178,14 +178,16 @@ func (r *HetznerBareMetalMachineReconciler) reconcileDelete(ctx context.Context,
 		return result, nil
 	}
 	// Machine is deleted so remove the finalizer.
-	controllerutil.RemoveFinalizer(machineScope.BareMetalMachine, infrav1.BareMetalMachineFinalizer)
+	controllerutil.RemoveFinalizer(machineScope.BareMetalMachine, infrav1.HetznerBareMetalMachineFinalizer)
+	controllerutil.RemoveFinalizer(machineScope.BareMetalMachine, infrav1.DeprecatedBareMetalMachineFinalizer)
 
 	return result, nil
 }
 
 func (r *HetznerBareMetalMachineReconciler) reconcileNormal(ctx context.Context, machineScope *scope.BareMetalMachineScope) (reconcile.Result, error) {
 	// If the HetznerBareMetalMachine doesn't have our finalizer, add it.
-	controllerutil.AddFinalizer(machineScope.BareMetalMachine, infrav1.BareMetalMachineFinalizer)
+	controllerutil.AddFinalizer(machineScope.BareMetalMachine, infrav1.HetznerBareMetalMachineFinalizer)
+	controllerutil.RemoveFinalizer(machineScope.BareMetalMachine, infrav1.DeprecatedBareMetalMachineFinalizer)
 
 	// Register the finalizer immediately to avoid orphaning HetznerBareMetal resources on delete
 	if err := machineScope.PatchObject(ctx); err != nil {
diff --git a/controllers/hetznercluster_controller.go b/controllers/hetznercluster_controller.go
index 905ed3a9..232cb232 100644
--- a/controllers/hetznercluster_controller.go
+++ b/controllers/hetznercluster_controller.go
@@ -178,7 +178,9 @@ func (r *HetznerClusterReconciler) reconcileNormal(ctx context.Context, clusterS
 	hetznerCluster := clusterScope.HetznerCluster
 
 	// If the HetznerCluster doesn't have our finalizer, add it.
-	controllerutil.AddFinalizer(hetznerCluster, infrav1.ClusterFinalizer)
+	controllerutil.AddFinalizer(hetznerCluster, infrav1.HetznerClusterFinalizer)
+	controllerutil.RemoveFinalizer(hetznerCluster, infrav1.DeprecatedHetznerClusterFinalizer)
+
 	if err := clusterScope.PatchObject(ctx); err != nil {
 		return reconcile.Result{}, err
 	}
@@ -366,7 +368,8 @@ func (r *HetznerClusterReconciler) reconcileDelete(ctx context.Context, clusterS
 	}
 
 	// Cluster is deleted so remove the finalizer.
-	controllerutil.RemoveFinalizer(clusterScope.HetznerCluster, infrav1.ClusterFinalizer)
+	controllerutil.RemoveFinalizer(clusterScope.HetznerCluster, infrav1.HetznerClusterFinalizer)
+	controllerutil.RemoveFinalizer(clusterScope.HetznerCluster, infrav1.DeprecatedHetznerClusterFinalizer)
 
 	return reconcile.Result{}, nil
 }
diff --git a/docs/README.md b/docs/README.md
index a76ea2fb..087cb30e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -26,6 +26,7 @@ This is the official documentation of Cluster API Provider Hetzner. Before start
 - [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)
+- [Annotations](/docs/caph/03-reference/08-annotations.md)
 
 ## Development
 
diff --git a/docs/caph/03-reference/08-annotations.md b/docs/caph/03-reference/08-annotations.md
new file mode 100644
index 00000000..f9dd6c3a
--- /dev/null
+++ b/docs/caph/03-reference/08-annotations.md
@@ -0,0 +1,55 @@
+---
+
+title: Annotations
+
+---
+
+You can set [Kubernetes Annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) to instruct the Syself CAPH Controller to modify its behavior.
+
+## Overview of Annotations
+
+### capi.syself.com/wipe-disk
+
+| **Resource**    | [HetznerBareMetalHost](/docs/caph/03-reference/05-hetzner-bare-metal-host.md)                                                                                                                                                                                                                                       |
+| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Description** | This annotation instructs the Syself CAPH Controller to wipe the disk before provisioning the machine.                                                                                                                                                                                                              |
+| **Value**       | You can use the string `"all"` or a space-separated list of WWNs. For example, `"10:00:00:05:1e:7a:7a:00 eui.00253885910c8cec 0x500a07511bb48b25"`. If the value is empty, no disks will be wiped. The value `"all"` will wipe all disks on the bare-metal machine (not just the one given in the rootDeviceHints). |
+| **Auto-Remove** | Enabled: The annotation is removed after the disks are wiped.                                                                                                                                                                                                                                                       |
+
+### capi.syself.com/ignore-check-disk
+
+| **Resource**    | [HetznerBareMetalHost](/docs/caph/03-reference/05-hetzner-bare-metal-host.md)                                                                                                                                                                                                                         |
+| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Description** | This annotation instructs the Syself CAPH Controller to ignore the results of the check-disk step during machine provisioning. The check will be performed, and a Kubernetes Event will be created. However, if this annotation is set, provisioning will continue even if a faulty disk is detected. |
+| **Value**       | The value is ignored. If the annotation exists, this feature is enabled.                                                                                                                                                                                                                              |
+| **Auto-Remove** | Disabled: The annotation remains on the resource. It is up to the user to remove it.                                                                                                                                                                                                                  |
+
+### capi.syself.com/allow-empty-control-plane-address
+
+| **Resource**    | [HetznerCluster](/docs/caph/03-reference/02-hetzner-cluster.md)                                                            |
+| --------------- | -------------------------------------------------------------------------------------------------------------------------- |
+| **Description** | This annotation allows the Syself CAPH Controller to create HetznerCluster resources with an empty `controlPlaneEndpoint`. This is useful to support externally managed control planes ([PR 1106](https://github.com/syself/cluster-api-provider-hetzner/pull/1106)) |
+| **Value**       | `"true"` enables this feature. All other strings are considered `"false"`.                                                 |
+| **Auto-Remove** | Disabled: The annotation remains on the resource.                                                                          |
+
+### capi.syself.com/constant-bare-metal-hostname
+
+| **Resource**    | Cluster (CRD of Cluster-API), HetznerBareMetalMachine                                                        |
+| --------------- | ------------------------------------------------------------------------------------------------------------ |
+| **Description** | See [Using constant hostnames](/docs/caph/02-topics/05-baremetal/04-constant-hostnames.md) for more details. |
+| **Auto-Remove** | Disabled: The annotation remains on the resource.                                                            |
+
+### capi.syself.com/reboot
+
+| **Resource**    | [HetznerBareMetalHost](/docs/caph/03-reference/05-hetzner-bare-metal-host.md)                                                                                                                                                                                 |
+| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Description** | If this annotation is present, the bare-metal machine will be rebooted. This annotation is used by `HetznerBareMetalRemediation` (see [Machine Health Checks with Custom Remediation Template](/docs/caph/02-topics/06-advanced/04-custom-templates-mhc.md)). |
+| **Value**       | The value is ignored. If the annotation exists, this feature is enabled.                                                                                                                                                                                      |
+| **Auto-Remove** | Enabled: The annotation is removed after the reboot.                                                                                                                                                                                                          |
+
+### capi.syself.com/permanent-error
+
+| **Resource**    | [HetznerBareMetalHost](/docs/caph/03-reference/05-hetzner-bare-metal-host.md)                                                                                                                                                                                                                                                                |
+| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Description** | This annotation is set by the Syself CAPH Controller when a bare-metal machine enters the "permanent error" state. This indicates that human intervention is required (e.g., to fix a broken disk). After the root cause is resolved, the user must remove this annotation to allow the Controller to manage the HetznerBareMetalHost again. |
+| **Auto-Remove** | Disabled: The annotation must be removed by the user.                                                                                                                                                                                                                                                                                        |
diff --git a/images/caph/Dockerfile b/images/caph/Dockerfile
index 25783f2f..ffbbc614 100644
--- a/images/caph/Dockerfile
+++ b/images/caph/Dockerfile
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # Build the manager binary
-FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.22.6-bullseye@sha256:825f81571780b54e553c4d7f21081efd47cd125dd7a8b3343a4591aa02f4816a AS build
+FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.22.6-bullseye@sha256:0838740c99e64dde1916553b004bdb1bc321823be83bde3d2a1563882eaab7e6 AS build
 ARG TARGETOS TARGETARCH
 
 COPY . /src/cluster-api-provider-hetzner
diff --git a/pkg/secrets/secret.go b/pkg/secrets/secret.go
index 46a2d376..656b0a0c 100644
--- a/pkg/secrets/secret.go
+++ b/pkg/secrets/secret.go
@@ -35,7 +35,11 @@ import (
 
 const (
 	// SecretFinalizer is the finalizer for secrets.
-	SecretFinalizer = infrav1.ClusterFinalizer + "/secret"
+	SecretFinalizer = "infrastructure.cluster.x-k8s.io/caph-secret"
+
+	// DeprecatedSecretFinalizer contains the old string.
+	// The controller will automatically update to the new string.
+	DeprecatedSecretFinalizer = infrav1.DeprecatedHetznerClusterFinalizer + "/secret"
 )
 
 // SecretManager is a type for fetching Secrets whether or not they are in the
@@ -91,8 +95,8 @@ func (sm *SecretManager) claimSecret(ctx context.Context, secret *corev1.Secret,
 		}
 	}
 
-	if addFinalizer && !utils.StringInList(secret.Finalizers, SecretFinalizer) {
-		secret.Finalizers = append(secret.Finalizers, SecretFinalizer)
+	if addFinalizer && (controllerutil.AddFinalizer(secret, SecretFinalizer) ||
+		controllerutil.RemoveFinalizer(secret, DeprecatedSecretFinalizer)) {
 		needsUpdate = true
 	}
 
@@ -180,8 +184,8 @@ func (sm *SecretManager) ReleaseSecret(ctx context.Context, secret *corev1.Secre
 
 	// remove finalizer from secret to allow deletion if no other owner exists
 	if !foundOtherHetznerClusterOwner {
-		secret.Finalizers = utils.FilterStringFromList(
-			secret.Finalizers, SecretFinalizer)
+		controllerutil.RemoveFinalizer(secret, SecretFinalizer)
+		controllerutil.RemoveFinalizer(secret, DeprecatedSecretFinalizer)
 	}
 
 	secret.OwnerReferences = newOwnerRefs
diff --git a/pkg/services/baremetal/host/host.go b/pkg/services/baremetal/host/host.go
index 9fac0ebc..feed86e8 100644
--- a/pkg/services/baremetal/host/host.go
+++ b/pkg/services/baremetal/host/host.go
@@ -40,6 +40,7 @@ import (
 	"sigs.k8s.io/cluster-api/util/record"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 	infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1"
@@ -1803,7 +1804,8 @@ func (s *Service) actionDeprovisioning(_ context.Context) actionResult {
 }
 
 func (s *Service) actionDeleting(_ context.Context) actionResult {
-	s.scope.HetznerBareMetalHost.Finalizers = utils.FilterStringFromList(s.scope.HetznerBareMetalHost.Finalizers, infrav1.BareMetalHostFinalizer)
+	controllerutil.RemoveFinalizer(s.scope.HetznerBareMetalHost, infrav1.HetznerBareMetalHostFinalizer)
+	controllerutil.RemoveFinalizer(s.scope.HetznerBareMetalHost, infrav1.DeprecatedBareMetalHostFinalizer)
 	return deleteComplete{}
 }
 
diff --git a/pkg/services/hcloud/server/server.go b/pkg/services/hcloud/server/server.go
index bce934e0..7911f708 100644
--- a/pkg/services/hcloud/server/server.go
+++ b/pkg/services/hcloud/server/server.go
@@ -189,6 +189,23 @@ func (s *Service) Reconcile(ctx context.Context) (res reconcile.Result, err erro
 	return res, nil
 }
 
+// implements setting rate limit on hcloudmachine.
+func handleRateLimit(hm *infrav1.HCloudMachine, err error, functionName string, errMsg string) error {
+	// returns error if not a rate limit exceeded error
+	if !hcloud.IsError(err, hcloud.ErrorCodeRateLimitExceeded) {
+		return fmt.Errorf("%s: %w", errMsg, err)
+	}
+
+	// does not return error if machine is running and does not have a deletion timestamp
+	if hm.Status.Ready && hm.DeletionTimestamp.IsZero() {
+		return nil
+	}
+
+	// check for a rate limit exceeded error if the machine is not running or if machine has a deletion timestamp
+	hcloudutil.HandleRateLimitExceeded(hm, err, functionName)
+	return fmt.Errorf("%s: %w", errMsg, err)
+}
+
 // Delete implements delete method of server.
 func (s *Service) Delete(ctx context.Context) (res reconcile.Result, err error) {
 	server, err := s.findServer(ctx)
@@ -246,12 +263,11 @@ func (s *Service) reconcileNetworkAttachment(ctx context.Context, server *hcloud
 			ID: s.scope.HetznerCluster.Status.Network.ID,
 		},
 	}); err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "AttachServerToNetwork")
 		// check if network status is old and server is in fact already attached
 		if hcloud.IsError(err, hcloud.ErrorCodeServerAlreadyAttached) {
 			return nil
 		}
-		return fmt.Errorf("failed to attach server to network: %w", err)
+		return handleRateLimit(s.scope.HCloudMachine, err, "AttachServerToNetwork", "failed to attach server to network")
 	}
 
 	return nil
@@ -308,11 +324,11 @@ func (s *Service) reconcileLoadBalancerAttachment(ctx context.Context, server *h
 	}
 
 	if err := s.scope.HCloudClient.AddTargetServerToLoadBalancer(ctx, opts, loadBalancer); err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "AddTargetServerToLoadBalancer")
 		if hcloud.IsError(err, hcloud.ErrorCodeTargetAlreadyDefined) {
 			return reconcile.Result{}, nil
 		}
-		return reconcile.Result{}, fmt.Errorf("failed to add server %s with ID %d as target to load balancer: %w", server.Name, server.ID, err)
+		errMsg := fmt.Sprintf("failed to add server %s with ID %d as target to load balancer", server.Name, server.ID)
+		return reconcile.Result{}, handleRateLimit(s.scope.HCloudMachine, err, "AddTargetServerToLoadBalancer", errMsg)
 	}
 
 	record.Eventf(
@@ -429,8 +445,7 @@ func (s *Service) createServer(ctx context.Context) (*hcloud.Server, error) {
 	// get all ssh keys that are stored in HCloud API
 	sshKeysAPI, err := s.scope.HCloudClient.ListSSHKeys(ctx, hcloud.SSHKeyListOpts{})
 	if err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "ListSSHKeys")
-		return nil, fmt.Errorf("failed listing ssh heys from hcloud: %w", err)
+		return nil, handleRateLimit(s.scope.HCloudMachine, err, "ListSSHKeys", "failed listing ssh keys from hcloud")
 	}
 
 	// find matching keys and store them
@@ -479,8 +494,8 @@ func (s *Service) createServer(ctx context.Context) (*hcloud.Server, error) {
 			s.scope.Name(),
 			err,
 		)
-		err = errors.Join(errServerCreateNotPossible, err)
-		return nil, fmt.Errorf("failed to create HCloud server %s: %w", s.scope.HCloudMachine.Name, err)
+		errMsg := fmt.Sprintf("failed to create HCloud server %s", s.scope.HCloudMachine.Name)
+		return nil, handleRateLimit(s.scope.HCloudMachine, err, "CreateServer", errMsg)
 	}
 
 	// set ssh keys to status
@@ -497,8 +512,7 @@ func (s *Service) getServerImage(ctx context.Context) (*hcloud.Image, error) {
 	// Get server type so we can filter for images with correct architecture
 	serverType, err := s.scope.HCloudClient.GetServerType(ctx, string(s.scope.HCloudMachine.Spec.Type))
 	if err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "GetServerType")
-		return nil, fmt.Errorf("failed to get server type in HCloud: %w", err)
+		return nil, handleRateLimit(s.scope.HCloudMachine, err, "GetServerType", "failed to get server type in HCloud")
 	}
 	if serverType == nil {
 		conditions.MarkFalse(
@@ -522,8 +536,7 @@ func (s *Service) getServerImage(ctx context.Context) (*hcloud.Image, error) {
 
 	images, err := s.scope.HCloudClient.ListImages(ctx, listOpts)
 	if err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "ListImages")
-		return nil, fmt.Errorf("failed to list images by label in HCloud: %w", err)
+		return nil, handleRateLimit(s.scope.HCloudMachine, err, "ListImages", "failed to list images by label in HCloud")
 	}
 
 	// query for an existing image by name.
@@ -533,8 +546,7 @@ func (s *Service) getServerImage(ctx context.Context) (*hcloud.Image, error) {
 	}
 	imagesByName, err := s.scope.HCloudClient.ListImages(ctx, listOpts)
 	if err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "ListImages")
-		return nil, fmt.Errorf("failed to list images by name in HCloud: %w", err)
+		return nil, handleRateLimit(s.scope.HCloudMachine, err, "ListImages", "failed to list images by name in HCloud")
 	}
 
 	images = append(images, imagesByName...)
@@ -577,12 +589,11 @@ func (s *Service) handleServerStatusOff(ctx context.Context, server *hcloud.Serv
 		if time.Now().Before(serverAvailableCondition.LastTransitionTime.Time.Add(serverOffTimeout)) {
 			// Not yet timed out, try again to power on
 			if err := s.scope.HCloudClient.PowerOnServer(ctx, server); err != nil {
-				hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "PowerOnServer")
 				if hcloud.IsError(err, hcloud.ErrorCodeLocked) {
 					// if server is locked, we just retry again
 					return reconcile.Result{RequeueAfter: 30 * time.Second}, nil
 				}
-				return reconcile.Result{}, fmt.Errorf("failed to power on server: %w", err)
+				return reconcile.Result{}, handleRateLimit(s.scope.HCloudMachine, err, "PowerOnServer", "failed to power on server")
 			}
 		} else {
 			// Timed out. Set failure reason
@@ -592,12 +603,11 @@ func (s *Service) handleServerStatusOff(ctx context.Context, server *hcloud.Serv
 	} else {
 		// No condition set yet. Try to power server on.
 		if err := s.scope.HCloudClient.PowerOnServer(ctx, server); err != nil {
-			hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "PowerOnServer")
 			if hcloud.IsError(err, hcloud.ErrorCodeLocked) {
 				// if server is locked, we just retry again
 				return reconcile.Result{RequeueAfter: 30 * time.Second}, nil
 			}
-			return reconcile.Result{}, fmt.Errorf("failed to power on server: %w", err)
+			return reconcile.Result{}, handleRateLimit(s.scope.HCloudMachine, err, "PowerOnServer", "failed to power on server")
 		}
 		conditions.MarkFalse(
 			s.scope.HCloudMachine,
@@ -619,8 +629,7 @@ func (s *Service) handleDeleteServerStatusRunning(ctx context.Context, server *h
 
 	if s.scope.HasServerAvailableCondition() {
 		if err := s.scope.HCloudClient.ShutdownServer(ctx, server); err != nil {
-			hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "ShutdownServer")
-			return reconcile.Result{}, fmt.Errorf("failed to shutdown server: %w", err)
+			return reconcile.Result{}, handleRateLimit(s.scope.HCloudMachine, err, "ShutdownServer", "failed to shutdown server")
 		}
 
 		conditions.MarkFalse(s.scope.HCloudMachine,
@@ -635,9 +644,8 @@ func (s *Service) handleDeleteServerStatusRunning(ctx context.Context, server *h
 
 	// timeout for shutdown has been reached - delete server
 	if err := s.scope.HCloudClient.DeleteServer(ctx, server); err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "DeleteServer")
 		record.Warnf(s.scope.HCloudMachine, "FailedDeleteHCloudServer", "Failed to delete HCloud server %s", s.scope.Name())
-		return reconcile.Result{}, fmt.Errorf("failed to delete server: %w", err)
+		return reconcile.Result{}, handleRateLimit(s.scope.HCloudMachine, err, "DeleteServer", "failed to delete server")
 	}
 
 	record.Eventf(s.scope.HCloudMachine, "HCloudServerDeleted", "HCloud server %s deleted", s.scope.Name())
@@ -647,9 +655,8 @@ func (s *Service) handleDeleteServerStatusRunning(ctx context.Context, server *h
 func (s *Service) handleDeleteServerStatusOff(ctx context.Context, server *hcloud.Server) (res reconcile.Result, err error) {
 	// server is off and can be deleted
 	if err := s.scope.HCloudClient.DeleteServer(ctx, server); err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "DeleteServer")
 		record.Warnf(s.scope.HCloudMachine, "FailedDeleteHCloudServer", "Failed to delete HCloud server %s", s.scope.Name())
-		return reconcile.Result{}, fmt.Errorf("failed to delete server: %w", err)
+		return reconcile.Result{}, handleRateLimit(s.scope.HCloudMachine, err, "DeleteServer", "failed to delete server")
 	}
 
 	record.Eventf(s.scope.HCloudMachine, "HCloudServerDeleted", "HCloud server %s deleted", s.scope.Name())
@@ -660,12 +667,12 @@ func (s *Service) deleteServerOfLoadBalancer(ctx context.Context, server *hcloud
 	lb := &hcloud.LoadBalancer{ID: s.scope.HetznerCluster.Status.ControlPlaneLoadBalancer.ID}
 
 	if err := s.scope.HCloudClient.DeleteTargetServerOfLoadBalancer(ctx, lb, server); err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "DeleteTargetServerOfLoadBalancer")
 		// do not return an error in case the target was not found
 		if strings.Contains(err.Error(), "load_balancer_target_not_found") {
 			return nil
 		}
-		return fmt.Errorf("failed to delete server %s with ID %d as target of load balancer %s with ID %d: %w", server.Name, server.ID, lb.Name, lb.ID, err)
+		errMsg := fmt.Sprintf("failed to delete server %s with ID %d as target of load balancer %s with ID %d", server.Name, server.ID, lb.Name, lb.ID)
+		return handleRateLimit(s.scope.HCloudMachine, err, "DeleteTargetServerOfLoadBalancer", errMsg)
 	}
 	record.Eventf(
 		s.scope.HetznerCluster,
@@ -685,8 +692,8 @@ func (s *Service) findServer(ctx context.Context) (*hcloud.Server, error) {
 	if err == nil {
 		server, err = s.scope.HCloudClient.GetServer(ctx, serverID)
 		if err != nil {
-			hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "GetServer")
-			return nil, fmt.Errorf("failed to get server %d: %w", serverID, err)
+			errMsg := fmt.Sprintf("failed to get server %d", serverID)
+			return nil, handleRateLimit(s.scope.HCloudMachine, err, "GetServer", errMsg)
 		}
 
 		// if server has been found, return it
@@ -702,8 +709,7 @@ func (s *Service) findServer(ctx context.Context) (*hcloud.Server, error) {
 
 	servers, err := s.scope.HCloudClient.ListServers(ctx, opts)
 	if err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "ListServers")
-		return nil, fmt.Errorf("failed to list servers: %w", err)
+		return nil, handleRateLimit(s.scope.HCloudMachine, err, "ListServers", "failed to list servers")
 	}
 
 	if len(servers) > 1 {
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index 7fc84a56..20541a02 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -172,17 +172,6 @@ func StringInList(list []string, strToSearch string) bool {
 	return false
 }
 
-// FilterStringFromList produces a new string slice that does not
-// include the strToFilter argument.
-func FilterStringFromList(list []string, strToFilter string) (newList []string) {
-	for _, item := range list {
-		if item != strToFilter {
-			newList = append(newList, item)
-		}
-	}
-	return
-}
-
 // GenerateName takes a name as string pointer. It returns name if pointer is not nil, otherwise it returns fallback with random suffix.
 func GenerateName(name *string, fallback string) string {
 	if name != nil {
diff --git a/templates/cluster-templates/bases/hcloud-kcp-ubuntu.yaml b/templates/cluster-templates/bases/hcloud-kcp-ubuntu.yaml
index 64aa8435..9614a3c9 100644
--- a/templates/cluster-templates/bases/hcloud-kcp-ubuntu.yaml
+++ b/templates/cluster-templates/bases/hcloud-kcp-ubuntu.yaml
@@ -166,7 +166,7 @@ spec:
           nameserver 2606:4700:4700::1111
     preKubeadmCommands:
       - set -x
-      - export CONTAINERD=1.7.20 # update: datasource=github-tags depName=containerd/containerd extractVersion=^v(?<version>.*)$ versioning=semver
+      - export CONTAINERD=1.7.21 # update: datasource=github-tags depName=containerd/containerd extractVersion=^v(?<version>.*)$ versioning=semver
       - export KUBERNETES_VERSION=$(echo ${KUBERNETES_VERSION} | sed 's/^v//')
       - export TRIMMED_KUBERNETES_VERSION=$(echo ${KUBERNETES_VERSION} | sed 's/^v//' | awk -F . '{print $1 "." $2}')
       - ARCH=amd64
diff --git a/templates/cluster-templates/bases/hetznerbaremetal-kcp-ubuntu.yaml b/templates/cluster-templates/bases/hetznerbaremetal-kcp-ubuntu.yaml
index 98a110ed..561a1d78 100644
--- a/templates/cluster-templates/bases/hetznerbaremetal-kcp-ubuntu.yaml
+++ b/templates/cluster-templates/bases/hetznerbaremetal-kcp-ubuntu.yaml
@@ -185,7 +185,7 @@ spec:
           nameserver 2606:4700:4700::1111
     preKubeadmCommands:
       - set -x
-      - export CONTAINERD=1.7.20 # update: datasource=github-tags depName=containerd/containerd extractVersion=^v(?<version>.*)$ versioning=semver
+      - export CONTAINERD=1.7.21 # update: datasource=github-tags depName=containerd/containerd extractVersion=^v(?<version>.*)$ versioning=semver
       - export KUBERNETES_VERSION=$(echo ${KUBERNETES_VERSION} | sed 's/^v//')
       - export TRIMMED_KUBERNETES_VERSION=$(echo ${KUBERNETES_VERSION} | sed 's/^v//' | awk -F . '{print $1 "." $2}')
       - localectl set-locale LANG=en_US.UTF-8
diff --git a/templates/cluster-templates/bases/kct-md-0-ubuntu.yaml b/templates/cluster-templates/bases/kct-md-0-ubuntu.yaml
index 8c88b91d..7a785e7e 100644
--- a/templates/cluster-templates/bases/kct-md-0-ubuntu.yaml
+++ b/templates/cluster-templates/bases/kct-md-0-ubuntu.yaml
@@ -55,7 +55,7 @@ spec:
       preKubeadmCommands:
         - set -x
         - grep VERSION= /etc/os-release; uname -a
-        - export CONTAINERD=1.7.20 # update: datasource=github-tags depName=containerd/containerd extractVersion=^v(?<version>.*)$ versioning=semver
+        - export CONTAINERD=1.7.21 # update: datasource=github-tags depName=containerd/containerd extractVersion=^v(?<version>.*)$ versioning=semver
         - export KUBERNETES_VERSION=$(echo ${KUBERNETES_VERSION} | sed 's/^v//')
         - export TRIMMED_KUBERNETES_VERSION=$(echo ${KUBERNETES_VERSION} | sed 's/^v//' | awk -F . '{print $1 "." $2}')
         - ARCH=amd64


Test patch:
diff --git a/api/v1beta1/hetznerbaremetalhost_types_test.go b/api/v1beta1/hetznerbaremetalhost_types_test.go
index 9b7d205f..2f449d2a 100644
--- a/api/v1beta1/hetznerbaremetalhost_types_test.go
+++ b/api/v1beta1/hetznerbaremetalhost_types_test.go
@@ -406,8 +406,6 @@ var _ = Describe("Test ClearRebootAnnotations", func() {
 		expectAnnotations  map[string]string
 	}
 
-	secondRebootAnnotation := RebootAnnotation + "/"
-
 	DescribeTable("Test ClearRebootAnnotations",
 		func(tc testCaseClearRebootAnnotations) {
 			host := HetznerBareMetalHost{}
@@ -417,10 +415,6 @@ var _ = Describe("Test ClearRebootAnnotations", func() {
 
 			Expect(host.Annotations).Should(Equal(tc.expectAnnotations))
 		},
-		Entry("has reboot annotation - one annotation in list", testCaseClearRebootAnnotations{
-			currentAnnotations: map[string]string{secondRebootAnnotation: "reboot", RebootAnnotation: "reboot"},
-			expectAnnotations:  map[string]string{},
-		}),
 		Entry("has multiple reboot annotations - no other annotation in list", testCaseClearRebootAnnotations{
 			currentAnnotations: map[string]string{RebootAnnotation: "reboot"},
 			expectAnnotations:  map[string]string{},
@@ -430,7 +424,7 @@ var _ = Describe("Test ClearRebootAnnotations", func() {
 			expectAnnotations:  map[string]string{"other": "annotation"},
 		}),
 		Entry("has multiple reboot annotations - multiple annotations in list", testCaseClearRebootAnnotations{
-			currentAnnotations: map[string]string{secondRebootAnnotation: "reboot", "other": "annotation", RebootAnnotation: "reboot"},
+			currentAnnotations: map[string]string{"other": "annotation", RebootAnnotation: "reboot"},
 			expectAnnotations:  map[string]string{"other": "annotation"},
 		}),
 		Entry("has no reboot annotation", testCaseClearRebootAnnotations{
@@ -456,7 +450,7 @@ var _ = Describe("Test ClearRebootAnnotations", func() {
 		}),
 		Entry("reboot prefix", testCaseClearRebootAnnotations{
 			annotation: RebootAnnotation + "/" + "suffix",
-			expectBool: true,
+			expectBool: false,
 		}),
 		Entry("other annotation", testCaseClearRebootAnnotations{
 			annotation: "different/annotation",
diff --git a/controllers/hcloudremediation_controller_test.go b/controllers/hcloudremediation_controller_test.go
index cde728e5..c1910e11 100644
--- a/controllers/hcloudremediation_controller_test.go
+++ b/controllers/hcloudremediation_controller_test.go
@@ -258,7 +258,7 @@ var _ = Describe("HCloudRemediationReconciler", func() {
 			}, timeout).Should(BeTrue())
 		})
 
-		It("should delete machine if retry limit reached and reboot timed out", func() {
+		It("should delete machine if retry limit reached and reboot timed out (hcloud)", func() {
 			By("creating hcloudRemediation")
 			Expect(testEnv.Create(ctx, hcloudRemediation)).To(Succeed())
 
diff --git a/controllers/hetznerbaremetalhost_controller_test.go b/controllers/hetznerbaremetalhost_controller_test.go
index 326e54f5..9c77fc87 100644
--- a/controllers/hetznerbaremetalhost_controller_test.go
+++ b/controllers/hetznerbaremetalhost_controller_test.go
@@ -226,7 +226,7 @@ var _ = Describe("HetznerBareMetalHostReconciler", func() {
 					return false
 				}
 				for _, finalizer := range host.GetFinalizers() {
-					if finalizer == infrav1.BareMetalHostFinalizer {
+					if finalizer == infrav1.HetznerBareMetalHostFinalizer {
 						return true
 					}
 				}
diff --git a/controllers/hetznerbaremetalmachine_controller_test.go b/controllers/hetznerbaremetalmachine_controller_test.go
index b3769b6a..1f10f680 100644
--- a/controllers/hetznerbaremetalmachine_controller_test.go
+++ b/controllers/hetznerbaremetalmachine_controller_test.go
@@ -336,7 +336,7 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
 						return false
 					}
 					for _, finalizer := range bmMachine.GetFinalizers() {
-						if finalizer == infrav1.BareMetalMachineFinalizer {
+						if finalizer == infrav1.HetznerBareMetalMachineFinalizer {
 							return true
 						}
 					}
diff --git a/controllers/hetznerbaremetalremediation_controller_test.go b/controllers/hetznerbaremetalremediation_controller_test.go
index e4364119..633be58b 100644
--- a/controllers/hetznerbaremetalremediation_controller_test.go
+++ b/controllers/hetznerbaremetalremediation_controller_test.go
@@ -252,7 +252,7 @@ var _ = Describe("HetznerBareMetalRemediationReconciler", func() {
 				Expect(testEnv.Cleanup(ctx, testNs, hetznerSecret, osSSHSecret, rescueSSHSecret, bootstrapSecret)).To(Succeed())
 			})
 
-			Context("HetznerBaremetalHost doesn't exist", func() {
+			Context("HetznerBareMetalHost doesn't exist", func() {
 				AfterEach(func() {
 					Expect(testEnv.Cleanup(ctx, hetznerBareMetalRemediation, hetznerBaremetalMachine)).To(Succeed())
 				})
@@ -287,7 +287,7 @@ var _ = Describe("HetznerBareMetalRemediationReconciler", func() {
 				})
 			})
 
-			Context("HetznerBaremetalHost exist", func() {
+			Context("HetznerBareMetalHost exist", func() {
 				BeforeEach(func() {
 					hostKey = client.ObjectKey{Name: hostName, Namespace: testNs.Name}
 
@@ -358,7 +358,7 @@ var _ = Describe("HetznerBareMetalRemediationReconciler", func() {
 					}, timeout).Should(BeTrue())
 				})
 
-				It("should delete machine if retry limit reached and reboot timed out", func() {
+				It("should delete machine if retry limit reached and reboot timed out (bm)", func() {
 					By("creating hetznerBareMetalRemediation object")
 					Expect(testEnv.Create(ctx, hetznerBareMetalRemediation)).To(Succeed())
 
diff --git a/pkg/services/hcloud/server/server_test.go b/pkg/services/hcloud/server/server_test.go
index d7e2fc33..8f8d89fa 100644
--- a/pkg/services/hcloud/server/server_test.go
+++ b/pkg/services/hcloud/server/server_test.go
@@ -18,11 +18,13 @@ package server
 
 import (
 	"context"
+	"fmt"
 	"time"
 
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/ptr"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
@@ -339,3 +341,105 @@ var _ = Describe("Test ValidateLabels", func() {
 		}),
 	)
 })
+
+var _ = Describe("Test handleRateLimit", func() {
+	type testCaseHandleRateLimit struct {
+		hm              *infrav1.HCloudMachine
+		err             error
+		functionName    string
+		errMsg          string
+		expectError     error
+		expectCondition bool
+	}
+
+	DescribeTable("Test handleRateLimit",
+		func(tc testCaseHandleRateLimit) {
+			err := handleRateLimit(tc.hm, tc.err, tc.functionName, tc.errMsg)
+			if tc.expectError != nil {
+				Expect(err).To(MatchError(tc.expectError))
+			} else {
+				Expect(err).To(BeNil())
+			}
+			if tc.expectCondition {
+				Expect(isPresentAndFalseWithReason(tc.hm, infrav1.HetznerAPIReachableCondition, infrav1.RateLimitExceededReason)).To(BeTrue())
+			} else {
+				Expect(conditions.Get(tc.hm, infrav1.HetznerAPIReachableCondition)).To(BeNil())
+			}
+		},
+		Entry("machine not ready, rate limit exceeded error", testCaseHandleRateLimit{
+			hm: &infrav1.HCloudMachine{
+				Status: infrav1.HCloudMachineStatus{Ready: false},
+			},
+			err:             hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded},
+			functionName:    "TestFunction",
+			errMsg:          "Test error message",
+			expectError:     fmt.Errorf("Test error message: %w", hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded}),
+			expectCondition: true,
+		}),
+		Entry("machine has deletion timestamp, rate limit exceeded error", testCaseHandleRateLimit{
+			hm: &infrav1.HCloudMachine{
+				ObjectMeta: metav1.ObjectMeta{
+					DeletionTimestamp: &metav1.Time{Time: time.Now()},
+				},
+				Status: infrav1.HCloudMachineStatus{Ready: true},
+			},
+			err:             hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded},
+			functionName:    "TestFunction",
+			errMsg:          "Test error message",
+			expectError:     fmt.Errorf("Test error message: %w", hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded}),
+			expectCondition: true,
+		}),
+		Entry("machine not ready, has deletion timestamp, rate limit exceeded error", testCaseHandleRateLimit{
+			hm: &infrav1.HCloudMachine{
+				ObjectMeta: metav1.ObjectMeta{
+					DeletionTimestamp: &metav1.Time{Time: time.Now()},
+				},
+				Status: infrav1.HCloudMachineStatus{Ready: false},
+			},
+			err:             hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded},
+			functionName:    "TestFunction",
+			errMsg:          "Test error message",
+			expectError:     fmt.Errorf("Test error message: %w", hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded}),
+			expectCondition: true,
+		}),
+		Entry("machine ready, rate limit exceeded error", testCaseHandleRateLimit{
+			hm: &infrav1.HCloudMachine{
+				Status: infrav1.HCloudMachineStatus{Ready: true},
+			},
+			err:             hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded},
+			functionName:    "TestFunction",
+			errMsg:          "Test error message",
+			expectError:     nil,
+			expectCondition: false,
+		}),
+		Entry("machine ready, other error", testCaseHandleRateLimit{
+			hm: &infrav1.HCloudMachine{
+				Status: infrav1.HCloudMachineStatus{Ready: true},
+			},
+			err:             hcloud.Error{Code: hcloud.ErrorCodeResourceUnavailable},
+			functionName:    "TestFunction",
+			errMsg:          "Test error message",
+			expectError:     fmt.Errorf("Test error message: %w", hcloud.Error{Code: hcloud.ErrorCodeResourceUnavailable}),
+			expectCondition: false,
+		}),
+		Entry("machine not ready, other error", testCaseHandleRateLimit{
+			hm: &infrav1.HCloudMachine{
+				Status: infrav1.HCloudMachineStatus{Ready: false},
+			},
+			err:             hcloud.Error{Code: hcloud.ErrorCodeConflict},
+			functionName:    "TestFunction",
+			errMsg:          "Test conflict error message",
+			expectError:     fmt.Errorf("Test conflict error message: %w", hcloud.Error{Code: hcloud.ErrorCodeConflict}),
+			expectCondition: false,
+		}),
+	)
+})
+
+func isPresentAndFalseWithReason(getter conditions.Getter, condition clusterv1.ConditionType, reason string) bool {
+	if !conditions.Has(getter, condition) {
+		return false
+	}
+	objectCondition := conditions.Get(getter, condition)
+	return objectCondition.Status == corev1.ConditionFalse &&
+		objectCondition.Reason == reason
+}
diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go
index 0e09f4f8..742debe7 100644
--- a/pkg/utils/utils_test.go
+++ b/pkg/utils/utils_test.go
@@ -177,29 +177,6 @@ var _ = Describe("StringInList", func() {
 		}))
 })
 
-var _ = Describe("FilterStringFromList", func() {
-	type testCaseFilterStringFromList struct {
-		list            []string
-		str             string
-		expectedOutcome []string
-	}
-	DescribeTable("Test filter string from list",
-		func(tc testCaseFilterStringFromList) {
-			out := utils.FilterStringFromList(tc.list, tc.str)
-			Expect(out).To(Equal(tc.expectedOutcome))
-		},
-		Entry("entry1", testCaseFilterStringFromList{
-			list:            []string{"a", "b", "c"},
-			str:             "a",
-			expectedOutcome: []string{"b", "c"},
-		}),
-		Entry("entry2", testCaseFilterStringFromList{
-			list:            []string{"a", "b", "c"},
-			str:             "d",
-			expectedOutcome: []string{"a", "b", "c"},
-		}))
-})
-
 var _ = Describe("Test removeOwnerRefFromList", func() {
 	type testCaseRemoveOwnerRefFromList struct {
 		RefList         []metav1.OwnerReference