mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-18 04:54:54 +00:00
Support tls server name overrides in EgressSelectorConfiguration (#136640)
add integration test Use proper test header, change to etcdMain to recognize test flags fix goroutine leak in integration test redo integration test with kubeapiserverapptesting fix comment capitalization, use existing client libraries fix comment capitalization, use existing client libraries consolidate http connect handler logic from odic and tls_server-name into helper add expected SNI, remove unused test move oidc helpers.go to right dir, remove copyright year split helpers.go into descriptive file names use atomic ptr for SNI, refactor generateTestCerts, remove errors from runTLSEgressProxy, explain jwksHandler in comment use testify, add back context messages Clean up tests Signed-off-by: Monis Khan <mok@microsoft.com>
This commit is contained in:
@@ -146,6 +146,10 @@ type TLSConfig struct {
|
||||
// Must be configured if TCPTransport.URL is prefixed with https://
|
||||
// +optional
|
||||
ClientCert string
|
||||
|
||||
// tlsServerName is used to check server certificate. If tlsServerName is empty, the hostname used to contact the server is used.
|
||||
// +optional
|
||||
TLSServerName string
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
@@ -147,6 +147,10 @@ type TLSConfig struct {
|
||||
// Must be configured if TCPTransport.URL is prefixed with https://
|
||||
// +optional
|
||||
ClientCert string `json:"clientCert,omitempty"`
|
||||
|
||||
// tlsServerName is used to check server certificate. If tlsServerName is empty, the hostname used to contact the server is used.
|
||||
// +optional
|
||||
TLSServerName string `json:"tlsServerName,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
@@ -817,6 +817,7 @@ func autoConvert_v1alpha1_TLSConfig_To_apiserver_TLSConfig(in *TLSConfig, out *a
|
||||
out.CABundle = in.CABundle
|
||||
out.ClientKey = in.ClientKey
|
||||
out.ClientCert = in.ClientCert
|
||||
out.TLSServerName = in.TLSServerName
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -829,6 +830,7 @@ func autoConvert_apiserver_TLSConfig_To_v1alpha1_TLSConfig(in *apiserver.TLSConf
|
||||
out.CABundle = in.CABundle
|
||||
out.ClientKey = in.ClientKey
|
||||
out.ClientCert = in.ClientCert
|
||||
out.TLSServerName = in.TLSServerName
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,10 @@ type TLSConfig struct {
|
||||
// Must be configured if TCPTransport.URL is prefixed with https://
|
||||
// +optional
|
||||
ClientCert string `json:"clientCert,omitempty"`
|
||||
|
||||
// tlsServerName is used to check server certificate. If tlsServerName is empty, the hostname used to contact the server is used.
|
||||
// +optional
|
||||
TLSServerName string `json:"tlsServerName,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
@@ -753,6 +753,7 @@ func autoConvert_v1beta1_TLSConfig_To_apiserver_TLSConfig(in *TLSConfig, out *ap
|
||||
out.CABundle = in.CABundle
|
||||
out.ClientKey = in.ClientKey
|
||||
out.ClientCert = in.ClientCert
|
||||
out.TLSServerName = in.TLSServerName
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -765,6 +766,7 @@ func autoConvert_apiserver_TLSConfig_To_v1beta1_TLSConfig(in *apiserver.TLSConfi
|
||||
out.CABundle = in.CABundle
|
||||
out.ClientKey = in.ClientKey
|
||||
out.ClientCert = in.ClientCert
|
||||
out.TLSServerName = in.TLSServerName
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -302,6 +302,52 @@ spec:
|
||||
expectedResult: nil,
|
||||
expectedError: ptr.To("invalid service configuration object \"DaemonSet\""),
|
||||
},
|
||||
{
|
||||
name: "v1beta1 with tlsServerName",
|
||||
createFile: true,
|
||||
contents: `
|
||||
apiVersion: apiserver.k8s.io/v1beta1
|
||||
kind: EgressSelectorConfiguration
|
||||
egressSelections:
|
||||
- name: "cluster"
|
||||
connection:
|
||||
proxyProtocol: "HTTPConnect"
|
||||
transport:
|
||||
tcp:
|
||||
url: "https://proxy.kube-system.svc.cluster.local:8443"
|
||||
tlsConfig:
|
||||
caBundle: "/etc/srv/kubernetes/pki/konnectivity-server/ca.crt"
|
||||
clientKey: "/etc/srv/kubernetes/pki/konnectivity-server/client.key"
|
||||
clientCert: "/etc/srv/kubernetes/pki/konnectivity-server/client.crt"
|
||||
tlsServerName: "konnectivity-server.example.com"
|
||||
`,
|
||||
expectedResult: &apiserver.EgressSelectorConfiguration{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "",
|
||||
APIVersion: "",
|
||||
},
|
||||
EgressSelections: []apiserver.EgressSelection{
|
||||
{
|
||||
Name: "cluster",
|
||||
Connection: apiserver.Connection{
|
||||
ProxyProtocol: "HTTPConnect",
|
||||
Transport: &apiserver.Transport{
|
||||
TCP: &apiserver.TCPTransport{
|
||||
URL: "https://proxy.kube-system.svc.cluster.local:8443",
|
||||
TLSConfig: &apiserver.TLSConfig{
|
||||
CABundle: "/etc/srv/kubernetes/pki/konnectivity-server/ca.crt",
|
||||
ClientKey: "/etc/srv/kubernetes/pki/konnectivity-server/client.key",
|
||||
ClientCert: "/etc/srv/kubernetes/pki/konnectivity-server/client.crt",
|
||||
TLSServerName: "konnectivity-server.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
|
||||
@@ -300,6 +300,7 @@ func getTLSConfig(t *apiserver.TLSConfig) (*tls.Config, error) {
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{clientCerts},
|
||||
RootCAs: certPool,
|
||||
ServerName: t.TLSServerName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -29,6 +31,7 @@ import (
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apiserver/pkg/apis/apiserver"
|
||||
"k8s.io/apiserver/pkg/server/egressselector/metrics"
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
"k8s.io/component-base/metrics/legacyregistry"
|
||||
"k8s.io/component-base/metrics/testutil"
|
||||
testingclock "k8s.io/utils/clock/testing"
|
||||
@@ -343,3 +346,61 @@ konnectivity_network_proxy_client_client_connections{status="dialing"} 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTLSConfig(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
certPEM, keyPEM, err := certutil.GenerateSelfSignedCertKey("localhost", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate test certificates: %v", err)
|
||||
}
|
||||
|
||||
certPath := filepath.Join(tempDir, "cert.crt")
|
||||
keyPath := filepath.Join(tempDir, "cert.key")
|
||||
if err := os.WriteFile(certPath, certPEM, 0600); err != nil {
|
||||
t.Fatalf("Failed to write cert file: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
|
||||
t.Fatalf("Failed to write key file: %v", err)
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
tlsConfig *apiserver.TLSConfig
|
||||
expectedServerName string
|
||||
}{
|
||||
{
|
||||
name: "with TLSServerName set",
|
||||
tlsConfig: &apiserver.TLSConfig{
|
||||
CABundle: certPath,
|
||||
ClientCert: certPath,
|
||||
ClientKey: keyPath,
|
||||
TLSServerName: "custom-server.example.com",
|
||||
},
|
||||
expectedServerName: "custom-server.example.com",
|
||||
},
|
||||
{
|
||||
name: "without TLSServerName (empty)",
|
||||
tlsConfig: &apiserver.TLSConfig{
|
||||
CABundle: certPath,
|
||||
ClientCert: certPath,
|
||||
ClientKey: keyPath,
|
||||
TLSServerName: "",
|
||||
},
|
||||
expectedServerName: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tlsConfig, err := getTLSConfig(tc.tlsConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("getTLSConfig returned unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if tlsConfig.ServerName != tc.expectedServerName {
|
||||
t.Errorf("expected ServerName %q, got %q", tc.expectedServerName, tlsConfig.ServerName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
27
test/integration/apiserver/egressselector/main_test.go
Normal file
27
test/integration/apiserver/egressselector/main_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package egressselector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
framework.EtcdMain(m.Run)
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package egressselector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
"k8s.io/client-go/util/keyutil"
|
||||
kubeapiserverapptesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
testutils "k8s.io/kubernetes/test/utils"
|
||||
utilsoidc "k8s.io/kubernetes/test/utils/oidc"
|
||||
)
|
||||
|
||||
func generateTestCerts(t *testing.T, tempDir, serverName string) (caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath string) {
|
||||
t.Helper()
|
||||
|
||||
caKey, err := testutils.NewPrivateKey()
|
||||
require.NoError(t, err, "Failed to generate CA key")
|
||||
caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "Test CA"}, caKey)
|
||||
require.NoError(t, err, "Failed to create CA certificate")
|
||||
caCertPath = filepath.Join(tempDir, "ca.crt")
|
||||
require.NoError(t, os.WriteFile(caCertPath, testutils.EncodeCertPEM(caCert), 0600), "Failed to write CA cert")
|
||||
|
||||
serverKey, err := testutils.NewPrivateKey()
|
||||
require.NoError(t, err, "Failed to generate server key")
|
||||
serverCert, err := testutils.NewSignedCert(&certutil.Config{
|
||||
CommonName: serverName,
|
||||
AltNames: certutil.AltNames{DNSNames: []string{serverName}},
|
||||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
}, serverKey, caCert, caKey)
|
||||
require.NoError(t, err, "Failed to create server certificate")
|
||||
serverCertPath, serverKeyPath = writeCertAndKey(t, tempDir, "server", serverCert, serverKey)
|
||||
|
||||
clientKey, err := testutils.NewPrivateKey()
|
||||
require.NoError(t, err, "Failed to generate client key")
|
||||
clientCert, err := testutils.NewSignedCert(&certutil.Config{
|
||||
CommonName: "test-client",
|
||||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
}, clientKey, caCert, caKey)
|
||||
require.NoError(t, err, "Failed to create client certificate")
|
||||
clientCertPath, clientKeyPath = writeCertAndKey(t, tempDir, "client", clientCert, clientKey)
|
||||
|
||||
return caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath
|
||||
}
|
||||
|
||||
func writeCertAndKey(t *testing.T, dir, name string, cert *x509.Certificate, key crypto.Signer) (certPath, keyPath string) {
|
||||
t.Helper()
|
||||
certPath = filepath.Join(dir, name+".crt")
|
||||
keyPath = filepath.Join(dir, name+".key")
|
||||
require.NoError(t, os.WriteFile(certPath, testutils.EncodeCertPEM(cert), 0600))
|
||||
keyPEM, err := keyutil.MarshalPrivateKeyToPEM(key)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(keyPath, keyPEM, 0600))
|
||||
return certPath, keyPath
|
||||
}
|
||||
|
||||
// runTLSEgressProxy runs an HTTP CONNECT proxy with TLS.
|
||||
func runTLSEgressProxy(t *testing.T, serverCertPath, serverKeyPath, caCertPath string, called *atomic.Bool, observedSNI *atomic.Pointer[string]) string {
|
||||
t.Helper()
|
||||
|
||||
serverCert, err := tls.LoadX509KeyPair(serverCertPath, serverKeyPath)
|
||||
require.NoError(t, err, "Failed to load server cert")
|
||||
|
||||
clientCAs, err := certutil.NewPool(caCertPath)
|
||||
require.NoError(t, err, "Failed to load CA cert pool")
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
ClientCAs: clientCAs,
|
||||
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
observedSNI.Store(&hello.ServerName)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
listener, err := tls.Listen("tcp", "127.0.0.1:0", tlsConfig)
|
||||
require.NoError(t, err, "Failed to start TLS listener")
|
||||
|
||||
proxyAddr := listener.Addr().String()
|
||||
|
||||
server := &http.Server{Handler: utilsoidc.NewHTTPConnectProxyHandler(t, called)}
|
||||
|
||||
go func() {
|
||||
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
t.Logf("TLS egress proxy serve error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Cleanup(func() {
|
||||
if err := server.Close(); err != nil {
|
||||
t.Logf("Failed to close server: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return proxyAddr
|
||||
}
|
||||
|
||||
// TestTLSServerName verifies that the tlsServerName field in the egress selector configuration
|
||||
// correctly overrides SNI for TLS certificate validation when connecting through a proxy.
|
||||
func TestTLSServerName(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
proxyHostname := "egress-proxy.example.com"
|
||||
|
||||
caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath := generateTestCerts(t, tempDir, proxyHostname)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
tlsServerName string
|
||||
expectProxyCalled bool
|
||||
expectedSNI string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "matching tlsServerName succeeds TLS handshake",
|
||||
tlsServerName: proxyHostname,
|
||||
expectProxyCalled: true,
|
||||
expectedSNI: proxyHostname,
|
||||
description: "SNI override matches cert SANs, connection succeeds",
|
||||
},
|
||||
{
|
||||
name: "mismatched tlsServerName fails TLS handshake",
|
||||
tlsServerName: "wrong-hostname.example.com",
|
||||
expectProxyCalled: false,
|
||||
expectedSNI: "wrong-hostname.example.com",
|
||||
description: "SNI override doesn't match cert SANs, connection fails",
|
||||
},
|
||||
{
|
||||
name: "empty tlsServerName uses dial address and fails",
|
||||
tlsServerName: "",
|
||||
expectProxyCalled: false,
|
||||
expectedSNI: "",
|
||||
description: "Without tlsServerName, uses dial address 127.0.0.1 which fails cert validation",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var proxyCalled atomic.Bool
|
||||
var observedSNI atomic.Pointer[string]
|
||||
|
||||
proxyAddr := runTLSEgressProxy(t, serverCertPath, serverKeyPath, caCertPath, &proxyCalled, &observedSNI)
|
||||
t.Logf("TLS egress proxy ready at %s (cert issued for: %s)", proxyAddr, proxyHostname)
|
||||
|
||||
egressConfig := fmt.Sprintf(`
|
||||
apiVersion: apiserver.k8s.io/v1beta1
|
||||
kind: EgressSelectorConfiguration
|
||||
egressSelections:
|
||||
- name: cluster
|
||||
connection:
|
||||
proxyProtocol: HTTPConnect
|
||||
transport:
|
||||
tcp:
|
||||
url: https://%s
|
||||
tlsConfig:
|
||||
caBundle: %s
|
||||
clientCert: %s
|
||||
clientKey: %s
|
||||
tlsServerName: %s
|
||||
`, proxyAddr, caCertPath, clientCertPath, clientKeyPath, tc.tlsServerName)
|
||||
|
||||
const authenticationConfigWithEgress = `
|
||||
apiVersion: apiserver.config.k8s.io/v1
|
||||
kind: AuthenticationConfiguration
|
||||
jwt:
|
||||
- issuer:
|
||||
url: https://panda.snorlax
|
||||
audiences:
|
||||
- foo
|
||||
egressSelectorType: cluster
|
||||
claimMappings:
|
||||
username:
|
||||
expression: "'test-' + claims.sub"
|
||||
`
|
||||
|
||||
server := kubeapiserverapptesting.StartTestServerOrDie(
|
||||
t,
|
||||
kubeapiserverapptesting.NewDefaultTestServerOptions(),
|
||||
[]string{
|
||||
fmt.Sprintf("--egress-selector-config-file=%s", utilsoidc.WriteTempFile(t, egressConfig)),
|
||||
fmt.Sprintf("--authentication-config=%s", utilsoidc.WriteTempFile(t, authenticationConfigWithEgress)),
|
||||
},
|
||||
framework.SharedEtcd(),
|
||||
)
|
||||
t.Cleanup(server.TearDownFn)
|
||||
|
||||
var sni string
|
||||
err := wait.PollUntilContextTimeout(t.Context(), time.Second, wait.ForeverTestTimeout, true,
|
||||
func(ctx context.Context) (done bool, err error) {
|
||||
if ptr := observedSNI.Load(); ptr != nil {
|
||||
sni = *ptr
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
},
|
||||
)
|
||||
require.NoError(t, err, "SNI not observed")
|
||||
require.Equal(t, tc.expectedSNI, sni, "SNI mismatch")
|
||||
require.Equal(t, tc.expectProxyCalled, proxyCalled.Load(), "proxy called mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ package oidc
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
@@ -29,6 +28,7 @@ import (
|
||||
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
utilsoidc "k8s.io/kubernetes/test/utils/oidc"
|
||||
)
|
||||
|
||||
func runEgressProxy(t testing.TB, udsName string, ready chan<- struct{}) {
|
||||
@@ -41,75 +41,7 @@ func runEgressProxy(t testing.TB, udsName string, ready chan<- struct{}) {
|
||||
}
|
||||
|
||||
var called atomic.Bool
|
||||
httpConnectProxy := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/ready" {
|
||||
t.Log("egress proxy ready")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
called.Store(true)
|
||||
|
||||
if r.Method != http.MethodConnect {
|
||||
http.Error(w, "this proxy only supports CONNECT passthrough", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
backendConn, err := (&net.Dialer{}).DialContext(r.Context(), "tcp", r.Host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer func() { _ = backendConn.Close() }()
|
||||
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
requestHijackedConn, _, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer func() { _ = requestHijackedConn.Close() }()
|
||||
|
||||
// use t.Errorf for all errors after this Write since the client may think the connection is good
|
||||
_, err = requestHijackedConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected established error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
writerComplete := make(chan struct{})
|
||||
readerComplete := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(backendConn, requestHijackedConn)
|
||||
if err != nil && !utilnet.IsProbableEOF(err) {
|
||||
t.Errorf("unexpected writer error: %v", err)
|
||||
}
|
||||
close(writerComplete)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(requestHijackedConn, backendConn)
|
||||
if err != nil && !utilnet.IsProbableEOF(err) {
|
||||
t.Errorf("unexpected reader error: %v", err)
|
||||
}
|
||||
close(readerComplete)
|
||||
}()
|
||||
|
||||
// Wait for one half the connection to exit. Once it does,
|
||||
// the defer will clean up the other half of the connection.
|
||||
select {
|
||||
case <-writerComplete:
|
||||
case <-readerComplete:
|
||||
}
|
||||
})
|
||||
|
||||
server := http.Server{Handler: httpConnectProxy}
|
||||
server := http.Server{Handler: utilsoidc.NewHTTPConnectProxyHandler(t, &called)}
|
||||
|
||||
t.Cleanup(func() {
|
||||
if !called.Load() {
|
||||
|
||||
@@ -2219,7 +2219,7 @@ func startTestAPIServerForOIDC[L utilsoidc.JosePublicKey](t *testing.T, c apiSer
|
||||
|
||||
var customFlags []string
|
||||
if len(c.authenticationConfigYAML) > 0 {
|
||||
customFlags = []string{fmt.Sprintf("--authentication-config=%s", writeTempFile(t, c.authenticationConfigYAML))}
|
||||
customFlags = []string{fmt.Sprintf("--authentication-config=%s", utilsoidc.WriteTempFile(t, c.authenticationConfigYAML))}
|
||||
if c.needsEgressProxyOnStart {
|
||||
udsName := filepath.Join(t.TempDir(), "uds")
|
||||
ready := make(chan struct{})
|
||||
@@ -2241,7 +2241,7 @@ egressSelections:
|
||||
uds:
|
||||
udsName: %s
|
||||
`, udsName)
|
||||
customFlags = append(customFlags, fmt.Sprintf("--egress-selector-config-file=%s", writeTempFile(t, egressConfig)))
|
||||
customFlags = append(customFlags, fmt.Sprintf("--egress-selector-config-file=%s", utilsoidc.WriteTempFile(t, egressConfig)))
|
||||
}
|
||||
} else {
|
||||
customFlags = []string{
|
||||
@@ -2358,23 +2358,6 @@ func generateCert(t *testing.T) (cert, key []byte, certFilePath, keyFilePath str
|
||||
return cert, key, certFilePath, keyFilePath
|
||||
}
|
||||
|
||||
func writeTempFile(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
file, err := os.CreateTemp("", "oidc-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.Remove(file.Name()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
if err := os.WriteFile(file.Name(), []byte(content), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return file.Name()
|
||||
}
|
||||
|
||||
// indentCertificateAuthority indents the certificate authority to match
|
||||
// the format of the generated authentication config.
|
||||
func indentCertificateAuthority(caCert string) string {
|
||||
|
||||
103
test/utils/oidc/proxy.go
Normal file
103
test/utils/oidc/proxy.go
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
)
|
||||
|
||||
// NewHTTPConnectProxyHandler returns an http.Handler that implements an HTTP CONNECT proxy.
|
||||
// When a CONNECT request is received, it dials the target, hijacks the client connection,
|
||||
// and bidirectionally copies data between them. The called flag is set to true when a
|
||||
// non-ready request is received.
|
||||
func NewHTTPConnectProxyHandler(t testing.TB, called *atomic.Bool) http.Handler {
|
||||
t.Helper()
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/ready" {
|
||||
t.Log("egress proxy ready")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
called.Store(true)
|
||||
|
||||
if r.Method != http.MethodConnect {
|
||||
http.Error(w, "this proxy only supports CONNECT passthrough", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
backendConn, err := (&net.Dialer{}).DialContext(r.Context(), "tcp", r.Host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer func() { _ = backendConn.Close() }()
|
||||
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
clientConn, _, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer func() { _ = clientConn.Close() }()
|
||||
|
||||
// use t.Errorf for all errors after this Write since the client may think the connection is good
|
||||
_, err = clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected established error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
writerComplete := make(chan struct{})
|
||||
readerComplete := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(backendConn, clientConn)
|
||||
if err != nil && !utilnet.IsProbableEOF(err) {
|
||||
t.Logf("writer error: %v", err)
|
||||
}
|
||||
close(writerComplete)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(clientConn, backendConn)
|
||||
if err != nil && !utilnet.IsProbableEOF(err) {
|
||||
t.Logf("reader error: %v", err)
|
||||
}
|
||||
close(readerComplete)
|
||||
}()
|
||||
|
||||
// Wait for one half the connection to exit. Once it does,
|
||||
// the defer will clean up the other half of the connection.
|
||||
select {
|
||||
case <-writerComplete:
|
||||
case <-readerComplete:
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/go-jose/go-jose.v2"
|
||||
|
||||
"k8s.io/kubernetes/test/utils/oidc/handlers"
|
||||
)
|
||||
|
||||
@@ -255,3 +256,22 @@ func GetSignatureAlgorithm[K JoseKey](key K) jose.SignatureAlgorithm {
|
||||
panic("unknown key type") // should be impossible
|
||||
}
|
||||
}
|
||||
|
||||
// WriteTempFile writes content to a temporary file and returns its path.
|
||||
// The file is automatically cleaned up when the test completes.
|
||||
func WriteTempFile(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
file, err := os.CreateTemp("", "oidc-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := os.Remove(file.Name()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
if err := os.WriteFile(file.Name(), []byte(content), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return file.Name()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user