fix: tunnel apiserver controlplane egress through konnectivity (#991)

The EgressSelectorConfiguration written to every EKS control-plane VM
defined only the `cluster` egress selection, which carries kubelet
exec/logs/portforward. Admission webhooks and aggregated apiservers dial
via the `controlplane` selection instead, and an undefined selection
falls back to a direct dial. The control-plane VM sits alone in its own
VPC with no route to the service CIDR, so every webhook call timed out
after 10s and konnectivity never saw a single dial request for one.

Define both selections on the konnectivity-server UDS so webhook traffic
rides the agent-initiated reverse tunnel alongside kubelet traffic.
This commit is contained in:
Josh Siviour
2026-09-03 16:56:52 +10:00
committed by GitHub
parent 4d3ba022ce
commit 06df1b799b
2 changed files with 34 additions and 12 deletions
+20 -12
View File
@@ -87,9 +87,9 @@ const (
k3sConfigPath = "/etc/rancher/k3s/config.yaml"
// k3sEgressSelectorConfigPath is the EgressSelectorConfiguration the apiserver
// loads via --egress-selector-config-file. It points the `cluster` egress at the
// konnectivity-server UDS, so apiserver→pod/kubelet traffic (exec/logs/webhooks)
// rides the agent-initiated reverse tunnel instead of a direct (unroutable) IP.
// loads via --egress-selector-config-file. It points the `controlplane` and
// `cluster` egress at the konnectivity-server UDS, so apiserver→pod/kubelet
// traffic rides the agent-initiated reverse tunnel, not a direct IP.
k3sEgressSelectorConfigPath = "/etc/rancher/k3s/egress-selector-config.yaml"
// konnectivityUDSPath is the unix socket the konnectivity-server listens on for
@@ -469,20 +469,28 @@ func k3sServerJoinURL(ip string) string {
}
// egressSelectorConfigYAML renders the EgressSelectorConfiguration that wires the
// apiserver `cluster` egress to the konnectivity-server UDS. v1beta1 is the schema
// apiserver egress to the konnectivity-server UDS. v1beta1 is the schema
// k3s/kube-apiserver accept for --egress-selector-config-file.
func egressSelectorConfigYAML() string {
return strings.Join([]string{
lines := []string{
"apiVersion: apiserver.k8s.io/v1beta1",
"kind: EgressSelectorConfiguration",
"egressSelections:",
" - name: cluster",
" connection:",
" proxyProtocol: GRPC",
" transport:",
" uds:",
" udsName: " + konnectivityUDSPath,
}, "\n")
}
// controlplane carries admission-webhook and aggregated-apiserver dials;
// cluster carries kubelet exec/logs/portforward. Both must ride the tunnel:
// an undefined selection falls back to a direct, unroutable dial from the CP.
for _, name := range []string{"controlplane", "cluster"} {
lines = append(lines,
" - name: "+name,
" connection:",
" proxyProtocol: GRPC",
" transport:",
" uds:",
" udsName: "+konnectivityUDSPath,
)
}
return strings.Join(lines, "\n")
}
// dedupeNonEmpty returns the input with empty strings dropped and duplicates
@@ -440,6 +440,20 @@ func TestBuildK3sUserData_EgressSelectorDisabledWithKonnConfig(t *testing.T) {
"the EgressSelectorConfiguration file routes the cluster egress to the konn socket")
}
func TestEgressSelectorConfigYAML_TunnelsControlPlaneAndCluster(t *testing.T) {
cfg := egressSelectorConfigYAML()
// Admission webhooks and aggregated apiservers dial via `controlplane`; only
// kubelet exec/logs/portforward use `cluster`. Omitting controlplane leaves
// webhook dials direct, which cannot route from the CP VPC to the service CIDR.
assert.Contains(t, cfg, " - name: controlplane",
"webhook dials use the controlplane egress and must ride the konnectivity tunnel")
assert.Contains(t, cfg, " - name: cluster",
"kubelet exec/logs dials use the cluster egress")
assert.Equal(t, 2, strings.Count(cfg, "udsName: "+konnectivityUDSPath),
"both egress selections must terminate on the konnectivity socket")
}
func TestBuildK3sUserData_KonnectivityEnv(t *testing.T) {
in := validK3sInput()
in.PrivateEndpointIP = "10.32.100.4"