blob: e8e6fd80b38133c3914d89e3c5d9649f7baa020d [file] [log] [blame]
Lorenz Bruncc078df2021-12-23 11:51:55 +01001// Package authproxy implements an authenticating proxy in front of the K8s
2// API server converting Metropolis credentials into authentication headers.
3package authproxy
4
5import (
6 "context"
7 "crypto/tls"
8 "crypto/x509"
9 "encoding/json"
10 "fmt"
11 "net"
12 "net/http"
13 "net/http/httputil"
14 "net/url"
15 "strings"
16 "time"
17
18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19
20 "source.monogon.dev/metropolis/node"
21 "source.monogon.dev/metropolis/node/core/identity"
22 "source.monogon.dev/metropolis/node/kubernetes/pki"
23 "source.monogon.dev/metropolis/pkg/supervisor"
24)
25
26type Service struct {
27 // KPKI is a reference to the Kubernetes PKI
28 KPKI *pki.PKI
Serge Bazanskiad86a552024-01-31 17:46:47 +010029 // Node contains the node credentials
30 Node *identity.NodeCredentials
Lorenz Bruncc078df2021-12-23 11:51:55 +010031}
32
33func (s *Service) getTLSCert(ctx context.Context, name pki.KubeCertificateName) (*tls.Certificate, error) {
34 cert, key, err := s.KPKI.Certificate(ctx, name)
35 if err != nil {
36 return nil, fmt.Errorf("could not load certificate %q from PKI: %w", name, err)
37 }
38 parsedKey, err := x509.ParsePKCS8PrivateKey(key)
39 if err != nil {
40 return nil, fmt.Errorf("failed to parse key for cert %q: %w", name, err)
41 }
42 return &tls.Certificate{
43 Certificate: [][]byte{cert},
44 PrivateKey: parsedKey,
45 }, nil
46}
47
48func respondWithK8sStatus(w http.ResponseWriter, status *metav1.Status) error {
49 w.Header().Set("Content-Type", "application/json")
50 w.WriteHeader(int(status.Code))
51 return json.NewEncoder(w).Encode(status)
52}
53
54func (s *Service) Run(ctx context.Context) error {
55 logger := supervisor.Logger(ctx)
56
57 k8sCAs := x509.NewCertPool()
58 cert, _, err := s.KPKI.Certificate(ctx, pki.IdCA)
59 parsedCert, err := x509.ParseCertificate(cert)
60 if err != nil {
61 return fmt.Errorf("failed to parse K8s CA certificate: %w", err)
62 }
63 k8sCAs.AddCert(parsedCert)
64
65 clientCert, err := s.getTLSCert(ctx, pki.MetropolisAuthProxyClient)
66 if err != nil {
67 return err
68 }
69
Lorenz Brun79a1a8f2022-03-31 17:19:07 +020070 internalAPIServer := net.JoinHostPort("localhost", node.KubernetesAPIPort.PortString())
71 standardProxy := httputil.NewSingleHostReverseProxy(&url.URL{
Lorenz Bruncc078df2021-12-23 11:51:55 +010072 Scheme: "https",
Lorenz Brun79a1a8f2022-03-31 17:19:07 +020073 Host: internalAPIServer,
Lorenz Bruncc078df2021-12-23 11:51:55 +010074 })
Lorenz Brun79a1a8f2022-03-31 17:19:07 +020075 noHTTP2Proxy := httputil.NewSingleHostReverseProxy(&url.URL{
76 Scheme: "https",
77 Host: internalAPIServer,
78 })
79 transport := &http.Transport{
Lorenz Bruncc078df2021-12-23 11:51:55 +010080 DialContext: (&net.Dialer{
81 Timeout: 30 * time.Second,
82 KeepAlive: 30 * time.Second,
83 }).DialContext,
84 TLSClientConfig: &tls.Config{
85 RootCAs: k8sCAs,
86 Certificates: []tls.Certificate{*clientCert},
87 NextProtos: []string{"h2", "http/1.1"},
88 },
89 ForceAttemptHTTP2: true,
90 MaxIdleConns: 100,
91 IdleConnTimeout: 90 * time.Second,
92 TLSHandshakeTimeout: 10 * time.Second,
93 ExpectContinueTimeout: 1 * time.Second,
94 }
Lorenz Brun79a1a8f2022-03-31 17:19:07 +020095 standardProxy.Transport = transport
96 noHTTP2Transport := transport.Clone()
97 noHTTP2Transport.ForceAttemptHTTP2 = false
98 noHTTP2Transport.TLSClientConfig.NextProtos = []string{"http/1.1"}
99 noHTTP2Proxy.Transport = noHTTP2Transport
100 errorHandler := func(w http.ResponseWriter, req *http.Request, err error) {
Lorenz Bruncc078df2021-12-23 11:51:55 +0100101 logger.Infof("Proxy error: %v", err)
102 respondWithK8sStatus(w, &metav1.Status{
103 Status: metav1.StatusFailure,
104 Code: http.StatusBadGateway,
105 Reason: metav1.StatusReasonServiceUnavailable,
106 Message: "authproxy could not reach apiserver",
107 })
108 }
Lorenz Brun79a1a8f2022-03-31 17:19:07 +0200109 standardProxy.ErrorHandler = errorHandler
110 noHTTP2Proxy.ErrorHandler = errorHandler
Lorenz Bruncc078df2021-12-23 11:51:55 +0100111
Serge Bazanskiad86a552024-01-31 17:46:47 +0100112 serverCert := s.Node.TLSCredentials()
Lorenz Bruncc078df2021-12-23 11:51:55 +0100113 clientCAs := x509.NewCertPool()
114 clientCAs.AddCert(s.Node.ClusterCA())
115 server := &http.Server{
116 Addr: ":" + node.KubernetesAPIWrappedPort.PortString(),
117 TLSConfig: &tls.Config{
118 MinVersion: tls.VersionTLS12,
119 NextProtos: []string{"h2", "http/1.1"},
120 ClientAuth: tls.RequireAndVerifyClientCert,
121 ClientCAs: clientCAs,
Serge Bazanskiad86a552024-01-31 17:46:47 +0100122 Certificates: []tls.Certificate{serverCert},
Lorenz Bruncc078df2021-12-23 11:51:55 +0100123 },
124 // Limits match @io_k8s_apiserver/pkg/server:secure_serving.go Serve()
125 MaxHeaderBytes: 1 << 20,
126 IdleTimeout: 90 * time.Second,
127 ReadHeaderTimeout: 32 * time.Second,
128
129 Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
130 // Guaranteed to exist because of RequireAndVerifyClientCert
131 clientCert := req.TLS.VerifiedChains[0][0]
132 clientIdentity, err := identity.VerifyUserInCluster(clientCert, s.Node.ClusterCA())
133 if err != nil {
134 respondWithK8sStatus(rw, &metav1.Status{
135 Status: metav1.StatusFailure,
136 Code: http.StatusUnauthorized,
137 Reason: metav1.StatusReasonUnauthorized,
138 Message: fmt.Sprintf("Metropolis authentication failed: %v", err),
139 })
140 return
141 }
Lorenz Brun79a1a8f2022-03-31 17:19:07 +0200142 proxyToUse := standardProxy
143 // Kubernetes wants to use SPDY but using SPDY with HTTP/2 is unsupported.
144 // SPDY should be removed from K8s, this is tracked in
145 // https://github.com/kubernetes/kubernetes/issues/7452
146 if strings.HasPrefix(strings.ToLower(req.Header.Get("Upgrade")), "spdy/") {
147 proxyToUse = noHTTP2Proxy
148 }
Lorenz Bruncc078df2021-12-23 11:51:55 +0100149 // Clone the request as otherwise modifying it is not allowed
150 newReq := req.Clone(req.Context())
151 // Drop any X-Remote headers to prevent injection
152 for k := range newReq.Header {
153 if strings.HasPrefix(http.CanonicalHeaderKey(k), http.CanonicalHeaderKey("X-Remote-")) {
154 newReq.Header.Del(k)
155 }
156 }
157 newReq.Header.Set("X-Remote-User", clientIdentity)
158 newReq.Header.Set("X-Remote-Group", "")
159
Lorenz Brun79a1a8f2022-03-31 17:19:07 +0200160 proxyToUse.ServeHTTP(rw, newReq)
Lorenz Bruncc078df2021-12-23 11:51:55 +0100161 }),
162 }
163 go server.ListenAndServeTLS("", "")
164 logger.Info("K8s AuthProxy running")
165 <-ctx.Done()
166 return server.Close()
167}