diff --git a/lib/srv/alpnproxy/common/kube.go b/lib/srv/alpnproxy/common/kube.go index ae7f27efd2b2b..a4dc3710b439d 100644 --- a/lib/srv/alpnproxy/common/kube.go +++ b/lib/srv/alpnproxy/common/kube.go @@ -22,6 +22,8 @@ import ( "encoding/hex" "fmt" "strings" + + "github.com/gravitational/trace" ) // KubeLocalProxySNI generates the SNI used for Kube local proxy. @@ -37,6 +39,16 @@ func TeleportClusterFromKubeLocalProxySNI(serverName string) string { 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) + } + 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 { diff --git a/lib/srv/alpnproxy/kube.go b/lib/srv/alpnproxy/kube.go index 36ee94ce7fb93..27a285d31fa85 100644 --- a/lib/srv/alpnproxy/kube.go +++ b/lib/srv/alpnproxy/kube.go @@ -157,12 +157,20 @@ func writeKubeError(ctx context.Context, rw http.ResponseWriter, kubeError *apie } } +// ClearCerts clears the middleware certs. +// It will try to reissue them when a new request comes in. +func (m *KubeMiddleware) ClearCerts() { + m.certsMu.Lock() + defer m.certsMu.Unlock() + clear(m.certs) +} + // 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) - if err != nil { + if err != nil && !trace.IsNotFound(err) { return false } @@ -221,22 +229,36 @@ 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. func (m *KubeMiddleware) reissueCertIfExpired(ctx context.Context, cert tls.Certificate, serverName string) error { - x509Cert, err := utils.TLSCertLeaf(cert) - if err != nil { - return trace.Wrap(err) + needsReissue := false + if len(cert.Certificate) == 0 { + m.logger.InfoContext(ctx, "missing TLS certificate, attempting to reissue a new one") + needsReissue = true + } else { + x509Cert, err := utils.TLSCertLeaf(cert) + if err != nil { + return trace.Wrap(err) + } + if err := utils.VerifyCertificateExpiry(x509Cert, m.clock); err != nil { + needsReissue = true + } } - if err := utils.VerifyCertificateExpiry(x509Cert, m.clock); err == nil { + if !needsReissue { return nil } if m.certReissuer == nil { - return trace.BadParameter("can't reissue expired proxy certificate - reissuer is not available") + return trace.BadParameter("can't reissue proxy certificate - reissuer is not available") } - - // If certificate has expired we try to reissue it. - identity, err := tlsca.FromSubject(x509Cert.Subject, x509Cert.NotAfter) + 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) + 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) @@ -247,11 +269,7 @@ func (m *KubeMiddleware) reissueCertIfExpired(ctx context.Context, cert tls.Cert go func() { defer m.isCertReissuingRunning.Store(false) - cluster := identity.TeleportCluster - if identity.RouteToCluster != "" { - cluster = identity.RouteToCluster - } - newCert, err := m.certReissuer(m.closeContext, cluster, identity.KubernetesCluster) + newCert, err := m.certReissuer(m.closeContext, teleportCluster, kubeCluster) if err == nil { m.certsMu.Lock() m.certs[serverName] = newCert diff --git a/lib/srv/alpnproxy/local_proxy_http_middleware.go b/lib/srv/alpnproxy/local_proxy_http_middleware.go index a2d619f6bd38b..b0aab1c539b65 100644 --- a/lib/srv/alpnproxy/local_proxy_http_middleware.go +++ b/lib/srv/alpnproxy/local_proxy_http_middleware.go @@ -38,6 +38,10 @@ type LocalProxyHTTPMiddleware interface { // OverwriteClientCerts overwrites the client certs used for upstream connection. OverwriteClientCerts(req *http.Request) ([]tls.Certificate, error) + + // ClearCerts clears the middleware certs. + // It will try to reissue them when a new request comes in. + ClearCerts() } // DefaultLocalProxyHTTPMiddleware provides default implementations for LocalProxyHTTPMiddleware. @@ -56,3 +60,4 @@ func (m *DefaultLocalProxyHTTPMiddleware) HandleResponse(resp *http.Response) er func (m *DefaultLocalProxyHTTPMiddleware) OverwriteClientCerts(req *http.Request) ([]tls.Certificate, error) { return nil, trace.NotImplemented("not implemented") } +func (m *DefaultLocalProxyHTTPMiddleware) ClearCerts() {} diff --git a/lib/teleterm/daemon/daemon.go b/lib/teleterm/daemon/daemon.go index d3528793a4b99..e7f6368608c49 100644 --- a/lib/teleterm/daemon/daemon.go +++ b/lib/teleterm/daemon/daemon.go @@ -789,6 +789,18 @@ func (s *Service) AssumeRole(ctx context.Context, req *api.AssumeRoleRequest) er return trace.Wrap(err) } + for _, gw := range s.gateways { + targetURI := gw.TargetURI() + if !(targetURI.GetRootClusterURI() == cluster.URI && targetURI.IsKube()) { + continue + } + kubeGw, err := gateway.AsKube(gw) + if err != nil { + s.cfg.Logger.ErrorContext(ctx, "Could not clear certs for kube when assuming request", "error", err, "target_uri", targetURI) + } + kubeGw.ClearCerts() + } + // We have to reconnect using the updated cert. return trace.Wrap(s.ClearCachedClientsForRoot(cluster.URI)) } diff --git a/lib/teleterm/gateway/interfaces.go b/lib/teleterm/gateway/interfaces.go index 27bc6735a2b9d..9578dbbb350b5 100644 --- a/lib/teleterm/gateway/interfaces.go +++ b/lib/teleterm/gateway/interfaces.go @@ -90,6 +90,9 @@ type Kube interface { // KubeconfigPath returns the path to the kubeconfig used to connect the // local proxy. KubeconfigPath() string + // ClearCerts clears the local proxy middleware certs. + // It will try to reissue them when a new request comes in. + ClearCerts() } // App defines an app gateway. diff --git a/lib/teleterm/gateway/kube.go b/lib/teleterm/gateway/kube.go index 1dccb4189accc..ee13cb8bc9d74 100644 --- a/lib/teleterm/gateway/kube.go +++ b/lib/teleterm/gateway/kube.go @@ -42,6 +42,15 @@ import ( type kube struct { *base + clearCertsFn func() +} + +// ClearCerts clears the local proxy middleware certs. +// It will try to reissue them when a new request comes in. +func (k *kube) ClearCerts() { + if k.clearCertsFn != nil { + k.clearCertsFn() + } } // KubeconfigPath returns the kubeconfig path that can be used for clients to @@ -62,7 +71,7 @@ func makeKubeGateway(cfg Config) (Kube, error) { return nil, trace.Wrap(err) } - k := &kube{base} + k := &kube{base: base} // Generate a new private key for the proxy. The client's existing private key may be // a hardware-backed private key, which cannot be added to the local proxy kube config. @@ -155,7 +164,7 @@ func (k *kube) makeALPNLocalProxyForKube(cas map[string]tls.Certificate) error { func (k *kube) makeKubeMiddleware() (alpnproxy.LocalProxyHTTPMiddleware, error) { certs := make(alpnproxy.KubeClientCerts) certs.Add(k.cfg.ClusterName, k.cfg.TargetName, k.cfg.Cert) - return alpnproxy.NewKubeMiddleware(alpnproxy.KubeMiddlewareConfig{ + middleware := alpnproxy.NewKubeMiddleware(alpnproxy.KubeMiddlewareConfig{ Certs: certs, CertReissuer: func(ctx context.Context, teleportCluster, kubeCluster string) (tls.Certificate, error) { cert, err := k.cfg.OnExpiredCert(ctx, k) @@ -165,7 +174,10 @@ func (k *kube) makeKubeMiddleware() (alpnproxy.LocalProxyHTTPMiddleware, error) // TODO(tross): update this when kube is converted to use slog. Logger: slog.Default(), CloseContext: k.closeContext, - }), nil + }) + + k.clearCertsFn = middleware.ClearCerts + return middleware, nil } func (k *kube) makeForwardProxyForKube() error {