diff --git a/go.mod b/go.mod index fbfb22f8..8ed7dd31 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/go-logr/logr v1.4.2 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 20aebdae..8edf609c 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= diff --git a/internal/options/kube.go b/internal/options/kube.go index 0e4b892a..1055a4a9 100644 --- a/internal/options/kube.go +++ b/internal/options/kube.go @@ -64,6 +64,10 @@ func (k kubeOpts) BearerToken() string { return k.config.BearerToken } +func (k kubeOpts) BearerTokenFile() string { + return k.config.BearerTokenFile +} + func (k kubeOpts) KubernetesControlPlaneURL() *url.URL { return &k.url } diff --git a/internal/options/listener.go b/internal/options/listener.go index 6461b267..d667bbb6 100644 --- a/internal/options/listener.go +++ b/internal/options/listener.go @@ -19,6 +19,7 @@ type ListenerOpts interface { ImpersonationGroupsRegexp() *regexp.Regexp PreferredUsernameClaim() string ReverseProxyTransport() (*http.Transport, error) + BearerTokenFile() string BearerToken() string SkipImpersonationReview() bool } diff --git a/internal/webserver/webserver.go b/internal/webserver/webserver.go index 88a0940f..703c6afe 100644 --- a/internal/webserver/webserver.go +++ b/internal/webserver/webserver.go @@ -11,11 +11,13 @@ import ( "net/http" "net/http/httputil" "net/textproto" + "os" "regexp" "strings" "time" "github.com/go-logr/logr" + "github.com/golang-jwt/jwt/v5" "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/pkg/errors" @@ -80,7 +82,9 @@ func NewKubeFilter(opts options.ListenerOpts, srv options.ServerOptions, gates f impersonationGroupsRegexp: opts.ImpersonationGroupsRegexp(), skipImpersonationReview: opts.SkipImpersonationReview(), reverseProxy: reverseProxy, + bearerTokenFile: opts.BearerTokenFile(), bearerToken: opts.BearerToken(), + bearerTokenExpirationTime: bearerExpirationTime(opts.BearerToken()), usernameClaimField: opts.PreferredUsernameClaim(), serverOptions: srv, log: ctrl.Log.WithName("proxy"), @@ -97,6 +101,8 @@ type kubeFilter struct { skipImpersonationReview bool reverseProxy *httputil.ReverseProxy bearerToken string + bearerTokenFile string + bearerTokenExpirationTime time.Time usernameClaimField string serverOptions options.ServerOptions log logr.Logger @@ -180,9 +186,9 @@ func (n *kubeFilter) handleRequest(request *http.Request, selector labels.Select n.log.V(4).Info("updating RawQuery", "query", q.Encode()) request.URL.RawQuery = q.Encode() - if len(n.bearerToken) > 0 { - n.log.V(4).Info("Updating the token", "token", n.bearerToken) - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", n.bearerToken)) + if len(n.BearerToken()) > 0 { + n.log.V(4).Info("Updating the token", "token", n.BearerToken()) + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", n.BearerToken())) } } @@ -203,8 +209,8 @@ func (n *kubeFilter) impersonateHandler(writer http.ResponseWriter, request *htt n.log.V(4).Info("impersonating for the current request", "username", username, "groups", groups, "uri", request.URL.Path) - if len(n.bearerToken) > 0 { - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", n.bearerToken)) + if len(n.BearerToken()) > 0 { + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", n.BearerToken())) } // Dropping malicious header connection // https://github.com/projectcapsule/capsule-proxy/issues/188 @@ -502,3 +508,29 @@ func (n *kubeFilter) removingHopByHopHeaders(request *http.Request) { request.Header.Del(connectionHeaderName) } + +func (n *kubeFilter) BearerToken() string { + if time.Now().After(n.bearerTokenExpirationTime) { + n.log.V(5).Info("Token expired. Reading new token from file", "token", n.bearerToken, "token file", n.bearerTokenFile) + token, _ := os.ReadFile(n.bearerTokenFile) + n.bearerToken = string(token) + n.bearerTokenExpirationTime = bearerExpirationTime(string(token)) + } + + return n.bearerToken +} + +func bearerExpirationTime(tokenString string) time.Time { + token, _, _ := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{}) + claims, _ := token.Claims.(jwt.MapClaims) + + var mil int64 + switch iat := claims["exp"].(type) { + case float64: + mil = int64(iat) + case json.Number: + mil, _ = iat.Int64() + } + + return time.Unix(mil, 0) +}