Summary
Add support for enabling proxy protocol on the control plane (kube-apiserver) load balancer service.
Background
The Hetzner Cloud load balancer currently has proxy protocol hardcoded to false. Proxy protocol allows the LB to pass the original client IP to the backend, which is needed for accurate client IP logging and rate limiting at the kube-apiserver level.
Scope
Proxy protocol is enabled only on the kube-apiserver LB service. Services listed under spec.controlPlaneLoadBalancer.extraServices are never affected — they always keep proxy protocol false.
Design
Setting spec.controlPlaneLoadBalancer.enableProxyProtocol: true behaves differently depending on whether the cluster is new or already exists:
New clusters
The kube-API LB service is created with proxy protocol immediately — no annotation check is performed. There are no pre-existing backends that could receive unexpected PROXY-protocol headers, so the safety gate is unnecessary.
Existing clusters (migration path)
Proxy protocol is activated only when both conditions hold:
spec.controlPlaneLoadBalancer.enableProxyProtocol: true is set on the HetznerCluster
- Every control-plane node in the workload cluster carries the annotation
capi.syself.com/proxy-protocol-for-controlplane-loadbalancer: "true"
The annotation is set by an external service (e.g. a node-preparation daemonset). CAPH reads it and never writes it. Enabling proxy protocol is a one-way operation — once switched on it is never turned back off.
Why wait for all CP nodes on existing clusters?
Requiring every CP node to carry the annotation enables a safe automated transition from a cluster that does not support proxy protocol to one that does. If proxy protocol were enabled on the LB before all backends were ready, nodes still expecting plain TCP connections would receive malformed PROXY-protocol headers — breaking the control plane. By waiting for all nodes to signal readiness, CAPH ensures the LB is only switched once every backend can handle the new protocol.
Implementation Plan
1. New field in both v1beta1 and v1beta2
Add enableProxyProtocol bool to LoadBalancerSpec in both API versions (i.e. under spec.controlPlaneLoadBalancer). Since it is a plain optional bool it maps 1:1 between versions and the generated converter handles it directly.
2. New annotation constant
Add ProxyProtocolForControlPlaneLoadBalancerAnnotation = "capi.syself.com/proxy-protocol-for-controlplane-loadbalancer" in api/v1beta2/hetznercluster_types.go. CAPH reads this annotation from CP nodes on existing clusters — it never writes it. For new clusters this annotation is never read.
3. Effective proxy protocol computation (controllers/hetznercluster_controller.go)
Before every LB reconciliation, computeEffectiveProxyProtocol runs and sets ClusterScope.EffectiveProxyProtocolForControlPlane. This value is only used by the migration path:
- If the spec flag is
false → effective is false (fast path, no WL cluster access)
- Otherwise, list all CP nodes (label
node-role.kubernetes.io/control-plane) in the workload cluster and check that every one has the annotation
- If the WL cluster is not yet reachable, effective is
false (retried on next reconcile)
4. Load balancer service reconciliation (pkg/services/hcloud/loadbalancer/loadbalancer.go)
- If the kube-API LB service does not yet exist (new cluster): create it with
enableProxyProtocol from the spec directly — no annotation check.
- If the kube-API LB service exists without proxy protocol (migration): read
scope.EffectiveProxyProtocolForControlPlane; when true, delete the service and recreate it with proxy protocol enabled. Extra services always remain false.
- Switching from
true back to false is not supported (one-way operation).
5. v1beta1 ↔ v1beta2 conversion
The field exists in both versions so the generated converter handles it with a direct field assignment.
6. Generated files
Run make generate to regenerate CRD YAML, deepcopy, and conversion files.
Acceptance Criteria
Summary
Add support for enabling proxy protocol on the control plane (kube-apiserver) load balancer service.
Background
The Hetzner Cloud load balancer currently has proxy protocol hardcoded to
false. Proxy protocol allows the LB to pass the original client IP to the backend, which is needed for accurate client IP logging and rate limiting at the kube-apiserver level.Scope
Proxy protocol is enabled only on the kube-apiserver LB service. Services listed under
spec.controlPlaneLoadBalancer.extraServicesare never affected — they always keep proxy protocolfalse.Design
Setting
spec.controlPlaneLoadBalancer.enableProxyProtocol: truebehaves differently depending on whether the cluster is new or already exists:New clusters
The kube-API LB service is created with proxy protocol immediately — no annotation check is performed. There are no pre-existing backends that could receive unexpected PROXY-protocol headers, so the safety gate is unnecessary.
Existing clusters (migration path)
Proxy protocol is activated only when both conditions hold:
spec.controlPlaneLoadBalancer.enableProxyProtocol: trueis set on theHetznerClustercapi.syself.com/proxy-protocol-for-controlplane-loadbalancer: "true"The annotation is set by an external service (e.g. a node-preparation daemonset). CAPH reads it and never writes it. Enabling proxy protocol is a one-way operation — once switched on it is never turned back off.
Why wait for all CP nodes on existing clusters?
Requiring every CP node to carry the annotation enables a safe automated transition from a cluster that does not support proxy protocol to one that does. If proxy protocol were enabled on the LB before all backends were ready, nodes still expecting plain TCP connections would receive malformed PROXY-protocol headers — breaking the control plane. By waiting for all nodes to signal readiness, CAPH ensures the LB is only switched once every backend can handle the new protocol.
Implementation Plan
1. New field in both v1beta1 and v1beta2
Add
enableProxyProtocol booltoLoadBalancerSpecin both API versions (i.e. underspec.controlPlaneLoadBalancer). Since it is a plain optional bool it maps 1:1 between versions and the generated converter handles it directly.2. New annotation constant
Add
ProxyProtocolForControlPlaneLoadBalancerAnnotation = "capi.syself.com/proxy-protocol-for-controlplane-loadbalancer"inapi/v1beta2/hetznercluster_types.go. CAPH reads this annotation from CP nodes on existing clusters — it never writes it. For new clusters this annotation is never read.3. Effective proxy protocol computation (
controllers/hetznercluster_controller.go)Before every LB reconciliation,
computeEffectiveProxyProtocolruns and setsClusterScope.EffectiveProxyProtocolForControlPlane. This value is only used by the migration path:false→ effective isfalse(fast path, no WL cluster access)node-role.kubernetes.io/control-plane) in the workload cluster and check that every one has the annotationfalse(retried on next reconcile)4. Load balancer service reconciliation (
pkg/services/hcloud/loadbalancer/loadbalancer.go)enableProxyProtocolfrom the spec directly — no annotation check.scope.EffectiveProxyProtocolForControlPlane; whentrue, delete the service and recreate it with proxy protocol enabled. Extra services always remainfalse.trueback tofalseis not supported (one-way operation).5. v1beta1 ↔ v1beta2 conversion
The field exists in both versions so the generated converter handles it with a direct field assignment.
6. Generated files
Run
make generateto regenerate CRD YAML, deepcopy, and conversion files.Acceptance Criteria
spec.controlPlaneLoadBalancer.enableProxyProtocolfield exists in both v1beta1 and v1beta2 CRDsfalse(default), LB behavior is unchangedenableProxyProtocol: true→ kube-API LB service is created with proxy protocol immediately, without waiting for node annotationsenableProxyProtocol: truebut CP nodes lack the annotation → proxy protocol stays disabledtrueand all CP nodes have the annotation → proxy protocol is enabled on the kube-API LB service and stays enabled permanentlyspec.controlPlaneLoadBalancer.extraServices) are never given proxy protocol