mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-19 10:10:29 +08:00
kube: URL-based local-proxy routing for tsh proxy kube (#66099)
* kube: URL-based local-proxy routing * kube: drop wildcard local CA and SNI prefix * kube: unexport kubeClusterKey * kube: add tests for local-proxy path helpers * kube: write error msg for old URL format * kube: support both formats to preserve compatibility * kube: drop misleading 'regenerate kubeconfig' error * kube: drop in-place request mutation * kube: fix tests for URL-based routing * kube: drop legacy SNI fallback in local proxy
This commit is contained in:
@@ -780,10 +780,10 @@ func kubeClientForLocalProxy(t *testing.T, kubeconfigPath, teleportCluster, kube
|
||||
CAData: config.Clusters[contextName].CertificateAuthorityData,
|
||||
CertData: config.AuthInfos[contextName].ClientCertificateData,
|
||||
KeyData: config.AuthInfos[contextName].ClientKeyData,
|
||||
ServerName: alpncommon.KubeLocalProxySNI(teleportCluster, kubeCluster),
|
||||
ServerName: teleportCluster,
|
||||
}
|
||||
client, err := kubernetes.NewForConfig(&rest.Config{
|
||||
Host: "https://" + teleportCluster,
|
||||
Host: "https://" + teleportCluster + alpncommon.KubeLocalProxyPathPrefix(teleportCluster, kubeCluster),
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
})
|
||||
|
||||
@@ -390,7 +390,11 @@ func TestALPNSNIProxyKube(t *testing.T) {
|
||||
// Teleport Proxy with a L7 LB in front.
|
||||
t.Run("ALPN connection upgrade", func(t *testing.T) {
|
||||
teleportCluster := suite.root.Config.Auth.ClusterName.GetClusterName()
|
||||
kubeCluster := "gke_project_europecentral2a_cluster1"
|
||||
// Must match the KubeCluster claim on the cert issued at the top of
|
||||
// the test (see kube.ProxyConfig above) — URL-based routing on the
|
||||
// upstream Teleport proxy verifies the cluster identifier in the
|
||||
// path matches the cert claim.
|
||||
kubeCluster := "root.example.com"
|
||||
|
||||
k8sClient := createALPNLocalKubeClient(t,
|
||||
suite.root.Config.Proxy.WebAddr,
|
||||
@@ -635,7 +639,7 @@ func TestKubePROXYProtocol(t *testing.T) {
|
||||
k8Client = createALPNLocalKubeClient(t,
|
||||
targetAddr,
|
||||
testCluster.Secrets.SiteName,
|
||||
kubeCluster,
|
||||
kubeClusterName,
|
||||
kubeConfig)
|
||||
}
|
||||
|
||||
@@ -660,7 +664,7 @@ func createALPNLocalKubeClient(t *testing.T, targetAddr utils.NetAddr, teleportC
|
||||
// Generate a self-signed CA for kube local proxy.
|
||||
localCAKey, localCACert, err := tlsca.GenerateSelfSignedCA(pkix.Name{
|
||||
CommonName: "localhost",
|
||||
}, []string{alpncommon.KubeLocalProxyWildcardDomain(teleportCluster)}, defaults.CATTL)
|
||||
}, []string{teleportCluster}, defaults.CATTL)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make a mock ALB which points to the Teleport Proxy Service. Then
|
||||
@@ -683,13 +687,13 @@ func createALPNLocalKubeClient(t *testing.T, targetAddr utils.NetAddr, teleportC
|
||||
fp := mustStartKubeForwardProxy(t, lp.GetAddr())
|
||||
|
||||
k8Client, err := kubernetes.NewForConfig(&rest.Config{
|
||||
Host: "https://" + teleportCluster,
|
||||
Host: "https://" + teleportCluster + alpncommon.KubeLocalProxyPathPrefix(teleportCluster, kubeCluster),
|
||||
Proxy: http.ProxyURL(mustParseURL(t, "http://"+fp.GetAddr())),
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
CAData: localCACert,
|
||||
CertData: localCACert, // Client uses same cert as local proxy server.
|
||||
KeyData: localCAKey,
|
||||
ServerName: alpncommon.KubeLocalProxySNI(teleportCluster, kubeCluster),
|
||||
ServerName: teleportCluster,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -119,9 +119,9 @@ func CreateLocalProxyConfig(originalKubeConfig *clientcmdapi.Config, localProxyV
|
||||
|
||||
config.Clusters[contextName] = &clientcmdapi.Cluster{
|
||||
ProxyURL: localProxyValues.LocalProxyURL,
|
||||
Server: localProxyValues.TeleportKubeClusterAddr,
|
||||
Server: localProxyValues.TeleportKubeClusterAddr + common.KubeLocalProxyPathPrefix(cluster.TeleportCluster, cluster.KubeCluster),
|
||||
CertificateAuthorityData: localProxyValues.LocalProxyCAs[cluster.TeleportCluster],
|
||||
TLSServerName: common.KubeLocalProxySNI(cluster.TeleportCluster, cluster.KubeCluster),
|
||||
TLSServerName: cluster.TeleportCluster,
|
||||
}
|
||||
setStringExtensionInCluster(config.Clusters[contextName], extProfileName, localProxyValues.TeleportProfileName)
|
||||
setStringExtensionInCluster(config.Clusters[contextName], extTeleClusterName, cluster.TeleportCluster)
|
||||
|
||||
@@ -185,9 +185,9 @@ func TestLocalProxy(t *testing.T) {
|
||||
// Check for root-cluster-kube1.
|
||||
wantConfig.Clusters["root-cluster-kube1"] = &clientcmdapi.Cluster{
|
||||
ProxyURL: "http://localhost:12345",
|
||||
Server: rootKubeClusterAddr,
|
||||
Server: rootKubeClusterAddr + "/v1/teleport/cm9vdC1jbHVzdGVy/a3ViZTE",
|
||||
CertificateAuthorityData: caData,
|
||||
TLSServerName: "6b75626531.root-cluster",
|
||||
TLSServerName: rootClusterName,
|
||||
LocationOfOrigin: kubeconfigPath,
|
||||
Extensions: map[string]runtime.Object{
|
||||
extProfileName: &runtime.Unknown{
|
||||
|
||||
@@ -19,38 +19,39 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
)
|
||||
|
||||
// KubeLocalProxySNI generates the SNI used for Kube local proxy.
|
||||
func KubeLocalProxySNI(teleportCluster, kubeCluster string) string {
|
||||
// Hex encode to hide "." in kube cluster name so wildcard cert can be used:
|
||||
// <hex-encoded-kube-cluster>.<teleport-cluster>
|
||||
return fmt.Sprintf("%s.%s", hex.EncodeToString([]byte(kubeCluster)), teleportCluster)
|
||||
// KubeLocalProxyPathPrefix returns the kubeconfig `server:` URL suffix that
|
||||
// encodes the (teleport cluster, kube cluster) pair for URL-based routing.
|
||||
// Format: /v1/teleport/<base64url(teleport)>/<base64url(kube)>. Mirrors the
|
||||
// path consumed by the Teleport proxy's single-cert kube handler and the
|
||||
// format emitted by tbot v2 (lib/tbot/services/k8s/output_v2.go).
|
||||
func KubeLocalProxyPathPrefix(teleportCluster, kubeCluster string) string {
|
||||
return "/v1/teleport/" +
|
||||
base64.RawURLEncoding.EncodeToString([]byte(teleportCluster)) + "/" +
|
||||
base64.RawURLEncoding.EncodeToString([]byte(kubeCluster))
|
||||
}
|
||||
|
||||
// TeleportClusterFromKubeLocalProxySNI returns Teleport cluster name from SNI.
|
||||
func TeleportClusterFromKubeLocalProxySNI(serverName string) string {
|
||||
_, teleportCluster, _ := strings.Cut(serverName, ".")
|
||||
return teleportCluster
|
||||
}
|
||||
|
||||
// KubeClusterFromKubeLocalProxySNI returns Kubernetes cluster name from SNI.
|
||||
func KubeClusterFromKubeLocalProxySNI(serverName string) (string, error) {
|
||||
kubeCluster, _, _ := strings.Cut(serverName, ".")
|
||||
str, err := hex.DecodeString(kubeCluster)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
// ClustersFromKubeLocalProxyPath parses the leading
|
||||
// /v1/teleport/<b64>/<b64> prefix of a request URL path and returns the
|
||||
// decoded (teleport cluster, kube cluster) pair.
|
||||
func ClustersFromKubeLocalProxyPath(path string) (teleportCluster, kubeCluster string, err error) {
|
||||
trimmed := strings.TrimPrefix(path, "/")
|
||||
parts := strings.SplitN(trimmed, "/", 5)
|
||||
if len(parts) < 4 || parts[0] != "v1" || parts[1] != "teleport" {
|
||||
return "", "", trace.BadParameter("invalid kube local proxy path %q", path)
|
||||
}
|
||||
return string(str), nil
|
||||
}
|
||||
|
||||
// KubeLocalProxyWildcardDomain returns the wildcard domain used to generate
|
||||
// local self-signed CA for provided Teleport cluster.
|
||||
func KubeLocalProxyWildcardDomain(teleportCluster string) string {
|
||||
return "*." + teleportCluster
|
||||
tcBytes, err := base64.RawURLEncoding.DecodeString(parts[2])
|
||||
if err != nil {
|
||||
return "", "", trace.Wrap(err, "decoding teleport cluster name from path")
|
||||
}
|
||||
kcBytes, err := base64.RawURLEncoding.DecodeString(parts[3])
|
||||
if err != nil {
|
||||
return "", "", trace.Wrap(err, "decoding kube cluster name from path")
|
||||
}
|
||||
return string(tcBytes), string(kcBytes), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2023 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestKubeLocalProxyPathPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
teleportCluster string
|
||||
kubeCluster string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "short names",
|
||||
teleportCluster: "root-cluster",
|
||||
kubeCluster: "kube1",
|
||||
want: "/v1/teleport/cm9vdC1jbHVzdGVy/a3ViZTE",
|
||||
},
|
||||
{
|
||||
name: "long kube cluster name (regression for #61439)",
|
||||
teleportCluster: "teleport.example.com",
|
||||
kubeCluster: "loooooooooooooooooooooooooooooooooooooooooong-kube-cluster-exceeding-sixty-three-chars",
|
||||
want: "/v1/teleport/dGVsZXBvcnQuZXhhbXBsZS5jb20/bG9vb29vb29vb29vb29vb29vb29vb29vb29vb29vb29vb29vb29vb29vb25nLWt1YmUtY2x1c3Rlci1leGNlZWRpbmctc2l4dHktdGhyZWUtY2hhcnM",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, KubeLocalProxyPathPrefix(tc.teleportCluster, tc.kubeCluster))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClustersFromKubeLocalProxyPath(t *testing.T) {
|
||||
t.Run("round-trip", func(t *testing.T) {
|
||||
const teleportCluster = "teleport.example.com"
|
||||
const kubeCluster = "loooooooooooooooooooooooooooooooooooooooooong-kube-cluster-exceeding-sixty-three-chars"
|
||||
|
||||
path := KubeLocalProxyPathPrefix(teleportCluster, kubeCluster) + "/api/v1/namespaces"
|
||||
tc, kc, err := ClustersFromKubeLocalProxyPath(path)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, teleportCluster, tc)
|
||||
require.Equal(t, kubeCluster, kc)
|
||||
})
|
||||
|
||||
t.Run("just the prefix", func(t *testing.T) {
|
||||
path := KubeLocalProxyPathPrefix("root-cluster", "kube1")
|
||||
tc, kc, err := ClustersFromKubeLocalProxyPath(path)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "root-cluster", tc)
|
||||
require.Equal(t, "kube1", kc)
|
||||
})
|
||||
|
||||
t.Run("prefix with trailing slash", func(t *testing.T) {
|
||||
path := KubeLocalProxyPathPrefix("root-cluster", "kube1") + "/"
|
||||
tc, kc, err := ClustersFromKubeLocalProxyPath(path)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "root-cluster", tc)
|
||||
require.Equal(t, "kube1", kc)
|
||||
})
|
||||
|
||||
t.Run("rejects bare API path", func(t *testing.T) {
|
||||
_, _, err := ClustersFromKubeLocalProxyPath("/api/v1/namespaces")
|
||||
require.ErrorContains(t, err, "invalid kube local proxy path")
|
||||
})
|
||||
|
||||
t.Run("rejects wrong leading component", func(t *testing.T) {
|
||||
_, _, err := ClustersFromKubeLocalProxyPath("/v2/teleport/abc/def/api/v1/namespaces")
|
||||
require.ErrorContains(t, err, "invalid kube local proxy path")
|
||||
})
|
||||
|
||||
t.Run("rejects missing kube cluster segment", func(t *testing.T) {
|
||||
_, _, err := ClustersFromKubeLocalProxyPath("/v1/teleport/abc")
|
||||
require.ErrorContains(t, err, "invalid kube local proxy path")
|
||||
})
|
||||
|
||||
t.Run("rejects non-base64url teleport cluster", func(t *testing.T) {
|
||||
_, _, err := ClustersFromKubeLocalProxyPath("/v1/teleport/not*base64/a3ViZTE")
|
||||
require.ErrorContains(t, err, "decoding teleport cluster")
|
||||
})
|
||||
|
||||
t.Run("rejects non-base64url kube cluster", func(t *testing.T) {
|
||||
_, _, err := ClustersFromKubeLocalProxyPath("/v1/teleport/cm9vdC1jbHVzdGVy/not*base64")
|
||||
require.ErrorContains(t, err, "decoding kube cluster")
|
||||
})
|
||||
}
|
||||
+57
-47
@@ -58,12 +58,17 @@ const certReissueClientWait = time.Second * 3
|
||||
// we give them longer time to perform the headless login flow.
|
||||
const certReissueClientWaitHeadless = defaults.HeadlessLoginTimeout
|
||||
|
||||
type kubeClusterKey struct {
|
||||
teleportCluster string
|
||||
kubeCluster string
|
||||
}
|
||||
|
||||
// KubeClientCerts is a map of Kubernetes client certs.
|
||||
type KubeClientCerts map[string]tls.Certificate
|
||||
type KubeClientCerts map[kubeClusterKey]tls.Certificate
|
||||
|
||||
// Add adds a tls.Certificate for a kube cluster.
|
||||
func (c KubeClientCerts) Add(teleportCluster, kubeCluster string, cert tls.Certificate) {
|
||||
c[common.KubeLocalProxySNI(teleportCluster, kubeCluster)] = cert
|
||||
c[kubeClusterKey{teleportCluster: teleportCluster, kubeCluster: kubeCluster}] = cert
|
||||
}
|
||||
|
||||
// KubeCertReissuer reissues a client certificate for a Kubernetes cluster.
|
||||
@@ -175,11 +180,24 @@ func (m *KubeMiddleware) ClearCerts() {
|
||||
clear(m.certs)
|
||||
}
|
||||
|
||||
// resolveClusterKey extracts the (teleport, kube) cluster pair for req
|
||||
// from the /v1/teleport/<b64>/<b64> prefix of the URL path.
|
||||
func (m *KubeMiddleware) resolveClusterKey(req *http.Request) (teleportCluster, kubeCluster string, err error) {
|
||||
return common.ClustersFromKubeLocalProxyPath(req.URL.Path)
|
||||
}
|
||||
|
||||
// HandleRequest checks if middleware has valid certificate for this request and
|
||||
// reissues it if needed. In case of reissuing error we write directly to the response and return true,
|
||||
// so caller won't continue processing the request.
|
||||
func (m *KubeMiddleware) HandleRequest(rw http.ResponseWriter, req *http.Request) bool {
|
||||
cert, err := m.getCertForRequest(req)
|
||||
teleportCluster, kubeCluster, err := m.resolveClusterKey(req)
|
||||
if err != nil {
|
||||
m.logger.WarnContext(req.Context(), "Invalid kube local proxy request", "path", req.URL.Path, "error", err)
|
||||
trace.WriteError(rw, trace.Wrap(err))
|
||||
return true
|
||||
}
|
||||
|
||||
cert, err := m.getCert(teleportCluster, kubeCluster)
|
||||
// If the cert is cleared using m.ClearCerts(), it won't be found.
|
||||
// This forces the middleware to issue a new cert on a new request.
|
||||
// This is used in access requests in Connect where we want to refresh certs without closing the proxy.
|
||||
@@ -187,7 +205,7 @@ func (m *KubeMiddleware) HandleRequest(rw http.ResponseWriter, req *http.Request
|
||||
return false
|
||||
}
|
||||
|
||||
err = m.reissueCertIfExpired(req.Context(), cert, req.TLS.ServerName)
|
||||
err = m.reissueCertIfExpired(req.Context(), cert, teleportCluster, kubeCluster)
|
||||
if err != nil {
|
||||
// If user input is required we return an error that will try to get user attention to the local proxy
|
||||
if errors.Is(err, ErrUserInputRequired) {
|
||||
@@ -205,7 +223,10 @@ func (m *KubeMiddleware) HandleRequest(rw http.ResponseWriter, req *http.Request
|
||||
}, m.logger)
|
||||
return true
|
||||
}
|
||||
m.logger.WarnContext(req.Context(), "Failed to reissue certificate for server", "server", req.TLS.ServerName)
|
||||
m.logger.WarnContext(req.Context(), "Failed to reissue certificate",
|
||||
"teleport_cluster", teleportCluster,
|
||||
"kube_cluster", kubeCluster,
|
||||
)
|
||||
trace.WriteError(rw, trace.Wrap(err))
|
||||
return true
|
||||
}
|
||||
@@ -214,41 +235,41 @@ func (m *KubeMiddleware) HandleRequest(rw http.ResponseWriter, req *http.Request
|
||||
}
|
||||
|
||||
// GetServerName implements [LocalProxyHTTPMiddleware].
|
||||
// In relay mode it returns the per-cluster SNI the upstream relay uses to identify the target kube cluster.
|
||||
func (m *KubeMiddleware) GetServerName(req *http.Request) (string, bool, error) {
|
||||
if !m.relay {
|
||||
// if we're not connecting to a Relay we should use the standard SNI
|
||||
// configured in the LocalProxy
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
if req.TLS == nil {
|
||||
return "", false, trace.BadParameter("expected a https request with a TLS connection state")
|
||||
}
|
||||
teleportCluster := common.TeleportClusterFromKubeLocalProxySNI(req.TLS.ServerName)
|
||||
if teleportCluster == "" {
|
||||
return "", false, trace.BadParameter("invalid Teleport cluster name in SNI %+q", req.TLS.ServerName)
|
||||
}
|
||||
kubeCluster, err := common.KubeClusterFromKubeLocalProxySNI(req.TLS.ServerName)
|
||||
tc, kc, err := m.resolveClusterKey(req)
|
||||
if err != nil {
|
||||
return "", false, trace.BadParameter("invalid Kubernetes cluster name in SNI %+q", req.TLS.ServerName)
|
||||
return "", false, trace.Wrap(err)
|
||||
}
|
||||
return kuberelay.FullSNIForKubeCluster(tc, kc), true, nil
|
||||
}
|
||||
|
||||
return kuberelay.FullSNIForKubeCluster(teleportCluster, kubeCluster), true, nil
|
||||
// getCert looks up the per-cluster client cert to use for an outbound kube
|
||||
// API request. Clusters are identified by the (teleport, kube) pair parsed
|
||||
// from the request URL path.
|
||||
func (m *KubeMiddleware) getCert(teleportCluster, kubeCluster string) (tls.Certificate, error) {
|
||||
key := kubeClusterKey{teleportCluster: teleportCluster, kubeCluster: kubeCluster}
|
||||
|
||||
m.certsMu.RLock()
|
||||
cert, ok := m.certs[key]
|
||||
m.certsMu.RUnlock()
|
||||
if !ok {
|
||||
return tls.Certificate{}, trace.NotFound("no client cert found for teleport cluster %q kube cluster %q", teleportCluster, kubeCluster)
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (m *KubeMiddleware) getCertForRequest(req *http.Request) (tls.Certificate, error) {
|
||||
if req.TLS == nil {
|
||||
return tls.Certificate{}, trace.BadParameter("expect a TLS request")
|
||||
tc, kc, err := m.resolveClusterKey(req)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
m.certsMu.RLock()
|
||||
cert, ok := m.certs[req.TLS.ServerName]
|
||||
m.certsMu.RUnlock()
|
||||
if !ok {
|
||||
return tls.Certificate{}, trace.NotFound("no client cert found for %v", req.TLS.ServerName)
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
return m.getCert(tc, kc)
|
||||
}
|
||||
|
||||
// GetClientCerts implements [LocalProxyHTTPMiddleware].
|
||||
@@ -263,9 +284,9 @@ func (m *KubeMiddleware) GetClientCerts(req *http.Request) ([]tls.Certificate, b
|
||||
// ErrUserInputRequired returned when user's input required to relogin and/or reissue new certificate.
|
||||
var ErrUserInputRequired = errors.New("user input required")
|
||||
|
||||
// reissueCertIfExpired checks if provided certificate has expired and reissues it if needed and replaces in the middleware certs.
|
||||
// serverName has a form of <hex-encoded-kube-cluster>.<teleport-cluster>.
|
||||
func (m *KubeMiddleware) reissueCertIfExpired(ctx context.Context, cert tls.Certificate, serverName string) error {
|
||||
// reissueCertIfExpired checks if provided certificate has expired and
|
||||
// reissues it if needed, replacing the entry in the middleware cert map.
|
||||
func (m *KubeMiddleware) reissueCertIfExpired(ctx context.Context, cert tls.Certificate, teleportCluster, kubeCluster string) error {
|
||||
needsReissue := false
|
||||
if len(cert.Certificate) == 0 {
|
||||
m.logger.InfoContext(ctx, "missing TLS certificate, attempting to reissue a new one")
|
||||
@@ -286,17 +307,6 @@ func (m *KubeMiddleware) reissueCertIfExpired(ctx context.Context, cert tls.Cert
|
||||
if m.certReissuer == nil {
|
||||
return trace.BadParameter("can't reissue proxy certificate - reissuer is not available")
|
||||
}
|
||||
teleportCluster := common.TeleportClusterFromKubeLocalProxySNI(serverName)
|
||||
if teleportCluster == "" {
|
||||
return trace.BadParameter("can't reissue proxy certificate - teleport cluster is empty")
|
||||
}
|
||||
kubeCluster, err := common.KubeClusterFromKubeLocalProxySNI(serverName)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "can't reissue proxy certificate - kube cluster name is invalid")
|
||||
}
|
||||
if kubeCluster == "" {
|
||||
return trace.BadParameter("can't reissue proxy certificate - kube cluster is empty")
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
// We start cert reissuing (with relogin if required) only if it's not running already.
|
||||
@@ -309,7 +319,7 @@ func (m *KubeMiddleware) reissueCertIfExpired(ctx context.Context, cert tls.Cert
|
||||
newCert, err := m.certReissuer(m.closeContext, teleportCluster, kubeCluster)
|
||||
if err == nil {
|
||||
m.certsMu.Lock()
|
||||
m.certs[serverName] = newCert
|
||||
m.certs.Add(teleportCluster, kubeCluster, newCert)
|
||||
m.certsMu.Unlock()
|
||||
}
|
||||
errCh <- err
|
||||
@@ -355,11 +365,11 @@ func NewKubeListener(casByTeleportCluster map[string]tls.Certificate) (net.Liste
|
||||
}
|
||||
listener, err := tls.Listen("tcp", "localhost:0", &tls.Config{
|
||||
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
config, ok := configs[common.TeleportClusterFromKubeLocalProxySNI(hello.ServerName)]
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("unknown Teleport cluster or invalid TLS server name %v", hello.ServerName)
|
||||
sni := hello.ServerName
|
||||
if config, ok := configs[sni]; ok {
|
||||
return config, nil
|
||||
}
|
||||
return config, nil
|
||||
return nil, trace.BadParameter("unknown Teleport cluster or invalid TLS server name %v", sni)
|
||||
},
|
||||
})
|
||||
return listener, trace.Wrap(err)
|
||||
@@ -425,7 +435,7 @@ func NewKubeForwardProxy(config KubeForwardProxyConfig) (*ForwardProxy, error) {
|
||||
func CreateKubeLocalCAs(key *keys.PrivateKey, teleportClusters []string) (map[string]tls.Certificate, error) {
|
||||
cas := make(map[string]tls.Certificate)
|
||||
for _, teleportCluster := range teleportClusters {
|
||||
ca, err := createLocalCA(key, time.Now().Add(defaults.CATTL), common.KubeLocalProxyWildcardDomain(teleportCluster))
|
||||
ca, err := createLocalCA(key, time.Now().Add(defaults.CATTL), teleportCluster)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -509,10 +509,10 @@ func TestKubeMiddleware(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("expired certificate is still reissued if request context expires", func(t *testing.T) {
|
||||
reqURL, err := url.Parse("https://example.test" + common.KubeLocalProxyPathPrefix(teleportCluster, "kube1") + "/api/v1/namespaces")
|
||||
require.NoError(t, err)
|
||||
req := &http.Request{
|
||||
TLS: &tls.ConnectionState{
|
||||
ServerName: common.KubeLocalProxySNI(teleportCluster, "kube1"),
|
||||
},
|
||||
URL: reqURL,
|
||||
}
|
||||
// we set request context to a context that is already canceled, so handler function will start reissuing
|
||||
// certificate goroutine and then will exit immediately.
|
||||
@@ -528,7 +528,7 @@ func TestKubeMiddleware(t *testing.T) {
|
||||
Clock: clockwork.NewFakeClockAt(now.Add(time.Hour * 2)),
|
||||
CloseContext: context.Background(),
|
||||
})
|
||||
err := km.CheckAndSetDefaults()
|
||||
err = km.CheckAndSetDefaults()
|
||||
require.NoError(t, err)
|
||||
|
||||
var rw *responsewriters.MemoryResponseWriter
|
||||
@@ -601,12 +601,16 @@ func TestKubeMiddleware(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
mustURL := func(s string) *url.URL {
|
||||
u, err := url.Parse(s)
|
||||
require.NoError(t, err)
|
||||
return u
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := http.Request{
|
||||
TLS: &tls.ConnectionState{
|
||||
ServerName: common.KubeLocalProxySNI(teleportCluster, tt.reqClusterName),
|
||||
},
|
||||
URL: mustURL("https://example.test" + common.KubeLocalProxyPathPrefix(teleportCluster, tt.reqClusterName) + "/api/v1/namespaces"),
|
||||
}
|
||||
km := NewKubeMiddleware(KubeMiddlewareConfig{
|
||||
Certs: tt.startCerts,
|
||||
|
||||
@@ -68,7 +68,7 @@ func TestKubeGateway(t *testing.T) {
|
||||
KubernetesCluster: kubeClusterName,
|
||||
}
|
||||
clock := clockwork.NewFakeClock()
|
||||
proxy := mustStartMockProxyWithKubeAPI(t, identity)
|
||||
proxy := mustStartMockProxyWithKubeAPI(t, identity, teleportClusterName, kubeClusterName)
|
||||
gateway, err := New(
|
||||
Config{
|
||||
Clock: clock,
|
||||
@@ -157,10 +157,10 @@ func kubeClientForLocalProxy(t *testing.T, kubeconfigPath, teleportCluster, kube
|
||||
CAData: kubeCAPEM,
|
||||
CertData: clientCertPEM,
|
||||
KeyData: config.AuthInfos[contextName].ClientKeyData,
|
||||
ServerName: common.KubeLocalProxySNI(teleportCluster, kubeCluster),
|
||||
ServerName: teleportCluster,
|
||||
}
|
||||
client, err := kubernetes.NewForConfig(&rest.Config{
|
||||
Host: "https://" + teleportCluster,
|
||||
Host: "https://" + teleportCluster + common.KubeLocalProxyPathPrefix(teleportCluster, kubeCluster),
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
})
|
||||
@@ -204,7 +204,7 @@ func (m *mockProxyWithKubeAPI) certPool() *x509.CertPool {
|
||||
return certPool
|
||||
}
|
||||
|
||||
func mustStartMockProxyWithKubeAPI(t *testing.T, identity tlsca.Identity) *mockProxyWithKubeAPI {
|
||||
func mustStartMockProxyWithKubeAPI(t *testing.T, identity tlsca.Identity, teleportCluster, kubeCluster string) *mockProxyWithKubeAPI {
|
||||
t.Helper()
|
||||
|
||||
netListener, err := net.Listen("tcp", "localhost:0")
|
||||
@@ -231,7 +231,11 @@ func mustStartMockProxyWithKubeAPI(t *testing.T, identity tlsca.Identity) *mockP
|
||||
ClientCAs: m.certPool(),
|
||||
})
|
||||
go func() {
|
||||
err := http.Serve(tlsListener, mockKubeAPIHandler(t))
|
||||
// Stand in for the Teleport proxy's singleCertHandler route, which
|
||||
// consumes the /v1/teleport/<b64>/<b64> prefix before dispatching to
|
||||
// the kube API handlers.
|
||||
handler := http.StripPrefix(common.KubeLocalProxyPathPrefix(teleportCluster, kubeCluster), mockKubeAPIHandler(t))
|
||||
err := http.Serve(tlsListener, handler)
|
||||
if err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -201,10 +201,10 @@ func sendRequestToKubeLocalProxy(t *testing.T, config *clientcmdapi.Config, tele
|
||||
CAData: config.Clusters[contextName].CertificateAuthorityData,
|
||||
CertData: config.AuthInfos[contextName].ClientCertificateData,
|
||||
KeyData: config.AuthInfos[contextName].ClientKeyData,
|
||||
ServerName: common.KubeLocalProxySNI(teleportCluster, kubeCluster),
|
||||
ServerName: teleportCluster,
|
||||
}
|
||||
restConfig := &rest.Config{
|
||||
Host: "https://" + teleportCluster,
|
||||
Host: "https://" + teleportCluster + common.KubeLocalProxyPathPrefix(teleportCluster, kubeCluster),
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user