blob: 957cb8ac1a18abe2c61cd1a6634a4e3c834a6716 [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
29 // Node contains the node identity
30 Node *identity.Node
31}
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
112 serverCert, err := s.getTLSCert(ctx, pki.APIServer)
113 if err != nil {
114 return err
115 }
116 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,
125 Certificates: []tls.Certificate{*serverCert},
126 },
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}