|  | // Package authproxy implements an authenticating proxy in front of the K8s | 
|  | // API server converting Metropolis credentials into authentication headers. | 
|  | package authproxy | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "crypto/tls" | 
|  | "crypto/x509" | 
|  | "encoding/json" | 
|  | "fmt" | 
|  | "net" | 
|  | "net/http" | 
|  | "net/http/httputil" | 
|  | "net/url" | 
|  | "strings" | 
|  | "time" | 
|  |  | 
|  | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 
|  |  | 
|  | "source.monogon.dev/metropolis/node" | 
|  | "source.monogon.dev/metropolis/node/core/identity" | 
|  | "source.monogon.dev/metropolis/node/kubernetes/pki" | 
|  | "source.monogon.dev/metropolis/pkg/supervisor" | 
|  | ) | 
|  |  | 
|  | type Service struct { | 
|  | // KPKI is a reference to the Kubernetes PKI | 
|  | KPKI *pki.PKI | 
|  | // Node contains the node identity | 
|  | Node *identity.Node | 
|  | } | 
|  |  | 
|  | func (s *Service) getTLSCert(ctx context.Context, name pki.KubeCertificateName) (*tls.Certificate, error) { | 
|  | cert, key, err := s.KPKI.Certificate(ctx, name) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("could not load certificate %q from PKI: %w", name, err) | 
|  | } | 
|  | parsedKey, err := x509.ParsePKCS8PrivateKey(key) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("failed to parse key for cert %q: %w", name, err) | 
|  | } | 
|  | return &tls.Certificate{ | 
|  | Certificate: [][]byte{cert}, | 
|  | PrivateKey:  parsedKey, | 
|  | }, nil | 
|  | } | 
|  |  | 
|  | func respondWithK8sStatus(w http.ResponseWriter, status *metav1.Status) error { | 
|  | w.Header().Set("Content-Type", "application/json") | 
|  | w.WriteHeader(int(status.Code)) | 
|  | return json.NewEncoder(w).Encode(status) | 
|  | } | 
|  |  | 
|  | func (s *Service) Run(ctx context.Context) error { | 
|  | logger := supervisor.Logger(ctx) | 
|  |  | 
|  | k8sCAs := x509.NewCertPool() | 
|  | cert, _, err := s.KPKI.Certificate(ctx, pki.IdCA) | 
|  | parsedCert, err := x509.ParseCertificate(cert) | 
|  | if err != nil { | 
|  | return fmt.Errorf("failed to parse K8s CA certificate: %w", err) | 
|  | } | 
|  | k8sCAs.AddCert(parsedCert) | 
|  |  | 
|  | clientCert, err := s.getTLSCert(ctx, pki.MetropolisAuthProxyClient) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | internalAPIServer := net.JoinHostPort("localhost", node.KubernetesAPIPort.PortString()) | 
|  | standardProxy := httputil.NewSingleHostReverseProxy(&url.URL{ | 
|  | Scheme: "https", | 
|  | Host:   internalAPIServer, | 
|  | }) | 
|  | noHTTP2Proxy := httputil.NewSingleHostReverseProxy(&url.URL{ | 
|  | Scheme: "https", | 
|  | Host:   internalAPIServer, | 
|  | }) | 
|  | transport := &http.Transport{ | 
|  | DialContext: (&net.Dialer{ | 
|  | Timeout:   30 * time.Second, | 
|  | KeepAlive: 30 * time.Second, | 
|  | }).DialContext, | 
|  | TLSClientConfig: &tls.Config{ | 
|  | RootCAs:      k8sCAs, | 
|  | Certificates: []tls.Certificate{*clientCert}, | 
|  | NextProtos:   []string{"h2", "http/1.1"}, | 
|  | }, | 
|  | ForceAttemptHTTP2:     true, | 
|  | MaxIdleConns:          100, | 
|  | IdleConnTimeout:       90 * time.Second, | 
|  | TLSHandshakeTimeout:   10 * time.Second, | 
|  | ExpectContinueTimeout: 1 * time.Second, | 
|  | } | 
|  | standardProxy.Transport = transport | 
|  | noHTTP2Transport := transport.Clone() | 
|  | noHTTP2Transport.ForceAttemptHTTP2 = false | 
|  | noHTTP2Transport.TLSClientConfig.NextProtos = []string{"http/1.1"} | 
|  | noHTTP2Proxy.Transport = noHTTP2Transport | 
|  | errorHandler := func(w http.ResponseWriter, req *http.Request, err error) { | 
|  | logger.Infof("Proxy error: %v", err) | 
|  | respondWithK8sStatus(w, &metav1.Status{ | 
|  | Status:  metav1.StatusFailure, | 
|  | Code:    http.StatusBadGateway, | 
|  | Reason:  metav1.StatusReasonServiceUnavailable, | 
|  | Message: "authproxy could not reach apiserver", | 
|  | }) | 
|  | } | 
|  | standardProxy.ErrorHandler = errorHandler | 
|  | noHTTP2Proxy.ErrorHandler = errorHandler | 
|  |  | 
|  | serverCert, err := s.getTLSCert(ctx, pki.APIServer) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | clientCAs := x509.NewCertPool() | 
|  | clientCAs.AddCert(s.Node.ClusterCA()) | 
|  | server := &http.Server{ | 
|  | Addr: ":" + node.KubernetesAPIWrappedPort.PortString(), | 
|  | TLSConfig: &tls.Config{ | 
|  | MinVersion:   tls.VersionTLS12, | 
|  | NextProtos:   []string{"h2", "http/1.1"}, | 
|  | ClientAuth:   tls.RequireAndVerifyClientCert, | 
|  | ClientCAs:    clientCAs, | 
|  | Certificates: []tls.Certificate{*serverCert}, | 
|  | }, | 
|  | // Limits match @io_k8s_apiserver/pkg/server:secure_serving.go Serve() | 
|  | MaxHeaderBytes:    1 << 20, | 
|  | IdleTimeout:       90 * time.Second, | 
|  | ReadHeaderTimeout: 32 * time.Second, | 
|  |  | 
|  | Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { | 
|  | // Guaranteed to exist because of RequireAndVerifyClientCert | 
|  | clientCert := req.TLS.VerifiedChains[0][0] | 
|  | clientIdentity, err := identity.VerifyUserInCluster(clientCert, s.Node.ClusterCA()) | 
|  | if err != nil { | 
|  | respondWithK8sStatus(rw, &metav1.Status{ | 
|  | Status:  metav1.StatusFailure, | 
|  | Code:    http.StatusUnauthorized, | 
|  | Reason:  metav1.StatusReasonUnauthorized, | 
|  | Message: fmt.Sprintf("Metropolis authentication failed: %v", err), | 
|  | }) | 
|  | return | 
|  | } | 
|  | proxyToUse := standardProxy | 
|  | // Kubernetes wants to use SPDY but using SPDY with HTTP/2 is unsupported. | 
|  | // SPDY should be removed from K8s, this is tracked in | 
|  | // https://github.com/kubernetes/kubernetes/issues/7452 | 
|  | if strings.HasPrefix(strings.ToLower(req.Header.Get("Upgrade")), "spdy/") { | 
|  | proxyToUse = noHTTP2Proxy | 
|  | } | 
|  | // Clone the request as otherwise modifying it is not allowed | 
|  | newReq := req.Clone(req.Context()) | 
|  | // Drop any X-Remote headers to prevent injection | 
|  | for k := range newReq.Header { | 
|  | if strings.HasPrefix(http.CanonicalHeaderKey(k), http.CanonicalHeaderKey("X-Remote-")) { | 
|  | newReq.Header.Del(k) | 
|  | } | 
|  | } | 
|  | newReq.Header.Set("X-Remote-User", clientIdentity) | 
|  | newReq.Header.Set("X-Remote-Group", "") | 
|  |  | 
|  | proxyToUse.ServeHTTP(rw, newReq) | 
|  | }), | 
|  | } | 
|  | go server.ListenAndServeTLS("", "") | 
|  | logger.Info("K8s AuthProxy running") | 
|  | <-ctx.Done() | 
|  | return server.Close() | 
|  | } |