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

Fix wrong context usage for reissuing expired certificate for tsh proxy kube. #43374

Merged
merged 5 commits into from
Jun 27, 2024

Conversation

AntonAM
Copy link
Contributor

@AntonAM AntonAM commented Jun 21, 2024

We should not use request's context for the procedure of reissuing certs, those requests are supposed to fail early in case of MFA requirement to bring user's attention to the Connect/tsh, so users can finish the procedure. Additional discussion here: https://gravitational.slack.com/archives/C03FJA391M3/p1718188745510909 . This PR introduces context for the kube proxy middleware so we have a cancelable context (important for Connect) and don't have to rely on request's context.

Changelog: Wait for user MFA input when reissuing expired certificates for a kube proxy.

@github-actions github-actions bot added the tsh tsh - Teleport's command line tool for logging into nodes running Teleport. label Jun 21, 2024
@AntonAM AntonAM requested a review from ravicious June 21, 2024 18:22
Copy link
Contributor

@marcoandredinis marcoandredinis left a comment

Choose a reason for hiding this comment

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

Left a few comments.
How hard would it be to add test coverage?

lib/srv/alpnproxy/kube.go Outdated Show resolved Hide resolved
lib/srv/alpnproxy/kube.go Outdated Show resolved Hide resolved
lib/srv/alpnproxy/kube.go Outdated Show resolved Hide resolved
@@ -245,7 +251,7 @@ func (m *KubeMiddleware) reissueCertIfExpired(ctx context.Context, cert tls.Cert
if identity.RouteToCluster != "" {
cluster = identity.RouteToCluster
}
newCert, err := m.certReissuer(ctx, cluster, identity.KubernetesCluster)
newCert, err := m.certReissuer(m.context, cluster, identity.KubernetesCluster)
Copy link
Member

@ravicious ravicious Jun 24, 2024

Choose a reason for hiding this comment

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

I've learned that storing a context in a struct should be avoided. But I also know that often it's hard to refactor code out of that pattern.

However, in the case of alpnproxy.LocalProxy, the TCP version of the proxy already utilizes contexts passed as arguments pretty well. Instead of storing a context on KubeMiddleware, what do you think about passing the context from LocalProxy.Start? This is the same thing that the TCP proxy does. It'd make it so that the context works the same between TCP middlewares and HTTP middlewares.

It'd probably warrant adding some comments so that people implementing middlewares understand what kind of context is passed to HandleRequest.

Patch
diff --git a/lib/srv/alpnproxy/auth_checker_middleware.go b/lib/srv/alpnproxy/auth_checker_middleware.go
index d61ad24f6dd..372d1218813 100644
--- a/lib/srv/alpnproxy/auth_checker_middleware.go
+++ b/lib/srv/alpnproxy/auth_checker_middleware.go
@@ -19,6 +19,7 @@
 package alpnproxy
 
 import (
+	"context"
 	"crypto/subtle"
 	"net/http"
 
@@ -55,7 +56,7 @@ func (m *AuthorizationCheckerMiddleware) CheckAndSetDefaults() error {
 }
 
 // HandleRequest checks Authorization header, which must be either missing or set to the secret value of a bearer token.
-func (m *AuthorizationCheckerMiddleware) HandleRequest(rw http.ResponseWriter, req *http.Request) bool {
+func (m *AuthorizationCheckerMiddleware) HandleRequest(ctx context.Context, rw http.ResponseWriter, req *http.Request) bool {
 	auth := req.Header.Get("Authorization")
 	if auth == "" {
 		m.Log.Debugf("No Authorization header present, ignoring request.")
diff --git a/lib/srv/alpnproxy/aws_local_proxy.go b/lib/srv/alpnproxy/aws_local_proxy.go
index 794fc6b9b78..6e10806eb79 100644
--- a/lib/srv/alpnproxy/aws_local_proxy.go
+++ b/lib/srv/alpnproxy/aws_local_proxy.go
@@ -19,6 +19,7 @@
 package alpnproxy
 
 import (
+	"context"
 	"net/http"
 	"strings"
 
@@ -109,7 +110,7 @@ func (m *AWSAccessMiddleware) CheckAndSetDefaults() error {
 // Note that the first sts:AssumeRole should be signed with the
 // local-proxy-generated credentials by the AWS client, while the second
 // request is signed with real credentials of the assumed role.
-func (m *AWSAccessMiddleware) HandleRequest(rw http.ResponseWriter, req *http.Request) bool {
+func (m *AWSAccessMiddleware) HandleRequest(ctx context.Context, rw http.ResponseWriter, req *http.Request) bool {
 	sigV4, err := awsutils.ParseSigV4(req.Header.Get(awsutils.AuthorizationHeader))
 	if err != nil {
 		m.Log.WithError(err).Error("Failed to parse AWS request authorization header.")
diff --git a/lib/srv/alpnproxy/azure_msi_middleware.go b/lib/srv/alpnproxy/azure_msi_middleware.go
index f612c325ca6..c64948282ff 100644
--- a/lib/srv/alpnproxy/azure_msi_middleware.go
+++ b/lib/srv/alpnproxy/azure_msi_middleware.go
@@ -19,6 +19,7 @@
 package alpnproxy
 
 import (
+	"context"
 	"crypto"
 	"encoding/json"
 	"fmt"
@@ -85,7 +86,7 @@ func (m *AzureMSIMiddleware) CheckAndSetDefaults() error {
 	return nil
 }
 
-func (m *AzureMSIMiddleware) HandleRequest(rw http.ResponseWriter, req *http.Request) bool {
+func (m *AzureMSIMiddleware) HandleRequest(ctx context.Context, rw http.ResponseWriter, req *http.Request) bool {
 	if req.Host == types.TeleportAzureMSIEndpoint {
 		if err := m.msiEndpoint(rw, req); err != nil {
 			m.Log.Warnf("Bad MSI request: %v", err)
diff --git a/lib/srv/alpnproxy/kube.go b/lib/srv/alpnproxy/kube.go
index fac6d8e143f..c51e75c078c 100644
--- a/lib/srv/alpnproxy/kube.go
+++ b/lib/srv/alpnproxy/kube.go
@@ -160,13 +160,13 @@ func writeKubeError(rw http.ResponseWriter, kubeError *apierrors.StatusError, lo
 // 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 {
+func (m *KubeMiddleware) HandleRequest(ctx context.Context, rw http.ResponseWriter, req *http.Request) bool {
 	cert, err := m.getCertForRequest(req)
 	if err != nil {
 		return false
 	}
 
-	err = m.reissueCertIfExpired(req.Context(), cert, req.TLS.ServerName)
+	err = m.reissueCertIfExpired(ctx, cert, req.TLS.ServerName)
 	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) {
@@ -251,7 +251,7 @@ func (m *KubeMiddleware) reissueCertIfExpired(ctx context.Context, cert tls.Cert
 			if identity.RouteToCluster != "" {
 				cluster = identity.RouteToCluster
 			}
-			newCert, err := m.certReissuer(m.context, cluster, identity.KubernetesCluster)
+			newCert, err := m.certReissuer(ctx, cluster, identity.KubernetesCluster)
 			if err == nil {
 				m.certsMu.Lock()
 				m.certs[serverName] = newCert
diff --git a/lib/srv/alpnproxy/local_proxy.go b/lib/srv/alpnproxy/local_proxy.go
index 0aa7e75a193..053ab7a4b43 100644
--- a/lib/srv/alpnproxy/local_proxy.go
+++ b/lib/srv/alpnproxy/local_proxy.go
@@ -363,7 +363,7 @@ func (l *LocalProxy) startHTTPAccessProxy(ctx context.Context) error {
 				}
 			}
 
-			if l.cfg.HTTPMiddleware.HandleRequest(rw, req) {
+			if l.cfg.HTTPMiddleware.HandleRequest(ctx, rw, req) {
 				return
 			}
 
diff --git a/lib/srv/alpnproxy/local_proxy_http_middleware.go b/lib/srv/alpnproxy/local_proxy_http_middleware.go
index a2d619f6bd3..74a5ac08eae 100644
--- a/lib/srv/alpnproxy/local_proxy_http_middleware.go
+++ b/lib/srv/alpnproxy/local_proxy_http_middleware.go
@@ -19,6 +19,7 @@
 package alpnproxy
 
 import (
+	"context"
 	"crypto/tls"
 	"net/http"
 
@@ -31,7 +32,7 @@ type LocalProxyHTTPMiddleware interface {
 	CheckAndSetDefaults() error
 
 	// HandleRequest returns true if requests has been handled and must not be processed further, false otherwise.
-	HandleRequest(rw http.ResponseWriter, req *http.Request) bool
+	HandleRequest(ctx context.Context, rw http.ResponseWriter, req *http.Request) bool
 
 	// HandleResponse processes the server response before sending it to the client.
 	HandleResponse(resp *http.Response) error

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that article is one sided, it mostly talks about per-request context usage. And we of course have this pattern in the code and we use it a lot. But there's a also pattern with closeContext, that isn't limited to a single request, but controls lifecycle on a broader scope. And we use this pattern a lot as well.

Within HandleRequest we already have a request-scoped context, which is stored in the request.Context() (that backward compatibility case described in the article) and passing second context I think just muddies the waters, and indeed you have a wrong usage of a context in your patch - context provided to m.reissueCertIfExpired should really be req.Context(), because it processes cancellation of the request itself inside. If we want to provide the closeContext in the call, we would need to provide 2 contexts to that function, which again I don't think makes it more clear. TCP proxy doesn't have HTTP request, hence no object containing per-request context automatically provided to it.

As Marco correctly pointed out we should rename introduced context to closeContext to make its intention clearer, but otherwise I think that works fine for this case.

Copy link
Member

Choose a reason for hiding this comment

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

I'm still not sold on the closeContext pattern, but that's mostly because I'm not a Go expert and I haven't seen an authoritative source speak in favor of it (like that blog post 😏). As such, I'm not going to push you on it. Assuming that the closeContext pattern is valid, your argumentation makes sense to me.

Copy link
Contributor

Choose a reason for hiding this comment

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

Go's stdlib has examples of context coming out of structs.
It's not exactly the same but, as an example, in the http server:
https://cs.opensource.google/go/go/+/refs/tags/go1.22.4:src/net/http/server.go;l=2898-2904

On the other hand, this is a pattern used in a lot of places in teleport's codebase.
That's why it felt strange to not name it closeContext

@@ -245,7 +251,7 @@ func (m *KubeMiddleware) reissueCertIfExpired(ctx context.Context, cert tls.Cert
if identity.RouteToCluster != "" {
cluster = identity.RouteToCluster
}
newCert, err := m.certReissuer(ctx, cluster, identity.KubernetesCluster)
newCert, err := m.certReissuer(m.context, cluster, identity.KubernetesCluster)
Copy link
Member

Choose a reason for hiding this comment

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

I'm still not sold on the closeContext pattern, but that's mostly because I'm not a Go expert and I haven't seen an authoritative source speak in favor of it (like that blog post 😏). As such, I'm not going to push you on it. Assuming that the closeContext pattern is valid, your argumentation makes sense to me.

@AntonAM AntonAM added this pull request to the merge queue Jun 27, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jun 27, 2024
@AntonAM AntonAM added this pull request to the merge queue Jun 27, 2024
Merged via the queue into master with commit 68eafbd Jun 27, 2024
37 checks passed
@AntonAM AntonAM deleted the anton/fix-proxy-kube-reissue-context branch June 27, 2024 19:26
@public-teleport-github-review-bot

@AntonAM See the table below for backport results.

Branch Result
branch/v14 Failed
branch/v15 Create PR
branch/v16 Create PR

AntonAM added a commit that referenced this pull request Jun 27, 2024
…xy kube. (#43374)

* Fix wrong context usage for reissuing expired certificate for tsh proxy kube.

* Rename context to closeContext

* Add test for request context expiration.

* Add missing context in tests.

* Remove flakiness from the test.
github-merge-queue bot pushed a commit that referenced this pull request Jun 28, 2024
…xy kube. (#43374) (#43614)

* Fix wrong context usage for reissuing expired certificate for tsh proxy kube.

* Rename context to closeContext

* Add test for request context expiration.

* Add missing context in tests.

* Remove flakiness from the test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport/branch/v14 backport/branch/v15 backport/branch/v16 kubernetes-access size/sm teleport-connect Issues related to Teleport Connect. tsh tsh - Teleport's command line tool for logging into nodes running Teleport.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants