Skip to content

feat: Enable proxy protocol for control plane load balancer #2112

Description

@guettli

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:

  1. spec.controlPlaneLoadBalancer.enableProxyProtocol: true is set on the HetznerCluster
  2. 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

  • spec.controlPlaneLoadBalancer.enableProxyProtocol field exists in both v1beta1 and v1beta2 CRDs
  • When the field is false (default), LB behavior is unchanged
  • New cluster with enableProxyProtocol: true → kube-API LB service is created with proxy protocol immediately, without waiting for node annotations
  • Existing cluster with enableProxyProtocol: true but CP nodes lack the annotation → proxy protocol stays disabled
  • Existing cluster: when the field is true and all CP nodes have the annotation → proxy protocol is enabled on the kube-API LB service and stays enabled permanently
  • Extra LB services (spec.controlPlaneLoadBalancer.extraServices) are never given proxy protocol
  • v1beta1 ↔ v1beta2 round-trip conversion preserves the field

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions