blob: 9b24d19edf33afb45c4eaaaabc4bbce66304fb5e [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"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020023 "source.monogon.dev/osbase/supervisor"
Lorenz Bruncc078df2021-12-23 11:51:55 +010024)
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)
Tim Windelschmidt096654a2024-04-18 23:10:19 +020059 if err != nil {
60 return fmt.Errorf("could not load certificate %q from PKI: %w", pki.IdCA, err)
61 }
Lorenz Bruncc078df2021-12-23 11:51:55 +010062 parsedCert, err := x509.ParseCertificate(cert)
63 if err != nil {
64 return fmt.Errorf("failed to parse K8s CA certificate: %w", err)
65 }
66 k8sCAs.AddCert(parsedCert)
67
68 clientCert, err := s.getTLSCert(ctx, pki.MetropolisAuthProxyClient)
69 if err != nil {
70 return err
71 }
72
Lorenz Brun79a1a8f2022-03-31 17:19:07 +020073 internalAPIServer := net.JoinHostPort("localhost", node.KubernetesAPIPort.PortString())
74 standardProxy := httputil.NewSingleHostReverseProxy(&url.URL{
Lorenz Bruncc078df2021-12-23 11:51:55 +010075 Scheme: "https",
Lorenz Brun79a1a8f2022-03-31 17:19:07 +020076 Host: internalAPIServer,
Lorenz Bruncc078df2021-12-23 11:51:55 +010077 })
Lorenz Brun79a1a8f2022-03-31 17:19:07 +020078 noHTTP2Proxy := httputil.NewSingleHostReverseProxy(&url.URL{
79 Scheme: "https",
80 Host: internalAPIServer,
81 })
82 transport := &http.Transport{
Lorenz Bruncc078df2021-12-23 11:51:55 +010083 DialContext: (&net.Dialer{
84 Timeout: 30 * time.Second,
85 KeepAlive: 30 * time.Second,
86 }).DialContext,
87 TLSClientConfig: &tls.Config{
88 RootCAs: k8sCAs,
89 Certificates: []tls.Certificate{*clientCert},
90 NextProtos: []string{"h2", "http/1.1"},
91 },
92 ForceAttemptHTTP2: true,
93 MaxIdleConns: 100,
94 IdleConnTimeout: 90 * time.Second,
95 TLSHandshakeTimeout: 10 * time.Second,
96 ExpectContinueTimeout: 1 * time.Second,
97 }
Lorenz Brun79a1a8f2022-03-31 17:19:07 +020098 standardProxy.Transport = transport
99 noHTTP2Transport := transport.Clone()
100 noHTTP2Transport.ForceAttemptHTTP2 = false
101 noHTTP2Transport.TLSClientConfig.NextProtos = []string{"http/1.1"}
102 noHTTP2Proxy.Transport = noHTTP2Transport
103 errorHandler := func(w http.ResponseWriter, req *http.Request, err error) {
Lorenz Bruncc078df2021-12-23 11:51:55 +0100104 logger.Infof("Proxy error: %v", err)
105 respondWithK8sStatus(w, &metav1.Status{
106 Status: metav1.StatusFailure,
107 Code: http.StatusBadGateway,
108 Reason: metav1.StatusReasonServiceUnavailable,
109 Message: "authproxy could not reach apiserver",
110 })
111 }
Lorenz Brun79a1a8f2022-03-31 17:19:07 +0200112 standardProxy.ErrorHandler = errorHandler
113 noHTTP2Proxy.ErrorHandler = errorHandler
Lorenz Bruncc078df2021-12-23 11:51:55 +0100114
Serge Bazanskiad86a552024-01-31 17:46:47 +0100115 serverCert := s.Node.TLSCredentials()
Lorenz Bruncc078df2021-12-23 11:51:55 +0100116 clientCAs := x509.NewCertPool()
117 clientCAs.AddCert(s.Node.ClusterCA())
118 server := &http.Server{
119 Addr: ":" + node.KubernetesAPIWrappedPort.PortString(),
120 TLSConfig: &tls.Config{
121 MinVersion: tls.VersionTLS12,
122 NextProtos: []string{"h2", "http/1.1"},
123 ClientAuth: tls.RequireAndVerifyClientCert,
124 ClientCAs: clientCAs,
Serge Bazanskiad86a552024-01-31 17:46:47 +0100125 Certificates: []tls.Certificate{serverCert},
Lorenz Bruncc078df2021-12-23 11:51:55 +0100126 },
127 // Limits match @io_k8s_apiserver/pkg/server:secure_serving.go Serve()
128 MaxHeaderBytes: 1 << 20,
129 IdleTimeout: 90 * time.Second,
130 ReadHeaderTimeout: 32 * time.Second,
131
132 Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
133 // Guaranteed to exist because of RequireAndVerifyClientCert
134 clientCert := req.TLS.VerifiedChains[0][0]
135 clientIdentity, err := identity.VerifyUserInCluster(clientCert, s.Node.ClusterCA())
136 if err != nil {
137 respondWithK8sStatus(rw, &metav1.Status{
138 Status: metav1.StatusFailure,
139 Code: http.StatusUnauthorized,
140 Reason: metav1.StatusReasonUnauthorized,
141 Message: fmt.Sprintf("Metropolis authentication failed: %v", err),
142 })
143 return
144 }
Lorenz Brun79a1a8f2022-03-31 17:19:07 +0200145 proxyToUse := standardProxy
146 // Kubernetes wants to use SPDY but using SPDY with HTTP/2 is unsupported.
147 // SPDY should be removed from K8s, this is tracked in
148 // https://github.com/kubernetes/kubernetes/issues/7452
149 if strings.HasPrefix(strings.ToLower(req.Header.Get("Upgrade")), "spdy/") {
150 proxyToUse = noHTTP2Proxy
151 }
Lorenz Bruncc078df2021-12-23 11:51:55 +0100152 // Clone the request as otherwise modifying it is not allowed
153 newReq := req.Clone(req.Context())
154 // Drop any X-Remote headers to prevent injection
155 for k := range newReq.Header {
156 if strings.HasPrefix(http.CanonicalHeaderKey(k), http.CanonicalHeaderKey("X-Remote-")) {
157 newReq.Header.Del(k)
158 }
159 }
160 newReq.Header.Set("X-Remote-User", clientIdentity)
161 newReq.Header.Set("X-Remote-Group", "")
162
Lorenz Brun79a1a8f2022-03-31 17:19:07 +0200163 proxyToUse.ServeHTTP(rw, newReq)
Lorenz Bruncc078df2021-12-23 11:51:55 +0100164 }),
165 }
166 go server.ListenAndServeTLS("", "")
167 logger.Info("K8s AuthProxy running")
168 <-ctx.Done()
169 return server.Close()
170}