Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reissue kube certs when assuming access request #50553

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lib/srv/alpnproxy/common/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"encoding/hex"
"fmt"
"strings"

"github.com/gravitational/trace"
)

// KubeLocalProxySNI generates the SNI used for Kube local proxy.
Expand All @@ -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 {
Expand Down
48 changes: 33 additions & 15 deletions lib/srv/alpnproxy/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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")
Comment on lines +252 to +261
Copy link
Contributor Author

@gzdunek gzdunek Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this in my local setup for a leaf kube cluster and it seemed to work fine. I don't know if there are cases where I'd need to check identity.RouteToCluster.

}

errCh := make(chan error, 1)
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/srv/alpnproxy/local_proxy_http_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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() {}
12 changes: 12 additions & 0 deletions lib/teleterm/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
3 changes: 3 additions & 0 deletions lib/teleterm/gateway/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 15 additions & 3 deletions lib/teleterm/gateway/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
Loading