fix: keep the apiserver controlplane egress direct

The controlplane egress selector also carries the authentication
token-webhook dial, which targets 127.0.0.1:8443 on the control-plane VM.
Tunnelling it through konnectivity hands that dial to a randomly chosen
agent, where nothing is listening, so the authenticator returns an error
rather than a decision and every bearer-token request fails closed with a
bare 401.

It only surfaced once a second agent existed. The control-plane node runs
an agent itself, so while it is the only backend every tunnelled dial
lands back home and authentication works by luck. The apiserver's webhook
client also holds its connection for the 90s IdleConnTimeout, so there is
one backend selection per connection rather than per request.

TestEKS/IRSAPod hit both conditions every run: it builds a kubeconfig and
then waits ~105s for its nodegroup to go ACTIVE before issuing any
request, which both expires the pooled connection and brings up the
cluster's second agent.

Only the cluster egress needs the tunnel, for kubelet exec/logs/
portforward. This matches the upstream reference configuration.
This commit is contained in:
joshsiv-mulga
2026-09-08 08:46:43 +10:00
parent a46f02cc63
commit 2e397d6e97
2 changed files with 24 additions and 24 deletions
+16 -16
View File
@@ -472,25 +472,25 @@ func k3sServerJoinURL(ip string) string {
// apiserver egress to the konnectivity-server UDS. v1beta1 is the schema
// k3s/kube-apiserver accept for --egress-selector-config-file.
func egressSelectorConfigYAML() string {
lines := []string{
// controlplane also carries the authentication token-webhook dial, which
// targets 127.0.0.1 on this node. Tunnelled, it reaches a random agent where
// nothing listens, and every bearer-token request fails closed with 401.
return strings.Join([]string{
"apiVersion: apiserver.k8s.io/v1beta1",
"kind: EgressSelectorConfiguration",
"egressSelections:",
}
// 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")
" - name: controlplane",
" connection:",
" proxyProtocol: Direct",
// cluster carries kubelet exec/logs/portforward, which must ride the
// tunnel: a direct dial to a node's kubelet is unroutable from the CP.
" - name: cluster",
" connection:",
" proxyProtocol: GRPC",
" transport:",
" uds:",
" udsName: " + konnectivityUDSPath,
}, "\n")
}
// dedupeNonEmpty returns the input with empty strings dropped and duplicates
+8 -8
View File
@@ -440,18 +440,18 @@ func TestBuildK3sUserData_EgressSelectorDisabledWithKonnConfig(t *testing.T) {
"the EgressSelectorConfiguration file routes the cluster egress to the konn socket")
}
func TestEgressSelectorConfigYAML_TunnelsControlPlaneAndCluster(t *testing.T) {
func TestEgressSelectorConfigYAML_TunnelsClusterOnly(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")
// The authentication token webhook listens on 127.0.0.1 on this node and is
// dialled through `controlplane`. Tunnelling it hands the dial to a random
// agent, where nothing listens, and every bearer token 401s.
assert.Contains(t, cfg, " - name: controlplane\n connection:\n proxyProtocol: Direct",
"the controlplane egress must stay direct so the local token webhook is reachable")
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")
assert.Equal(t, 1, strings.Count(cfg, "udsName: "+konnectivityUDSPath),
"only the cluster egress selection terminates on the konnectivity socket")
}
func TestBuildK3sUserData_KonnectivityEnv(t *testing.T) {