blob: 93737eca69ad4c88f70b16a5fbbb2d17a560bad1 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Bruncc078df2021-12-23 11:51:55 +01004// Package authproxy implements an authenticating proxy in front of the K8s
5// API server converting Metropolis credentials into authentication headers.
6package authproxy
7
8import (
9 "context"
10 "crypto/tls"
11 "crypto/x509"
12 "encoding/json"
13 "fmt"
14 "net"
15 "net/http"
16 "net/http/httputil"
17 "net/url"
18 "strings"
19 "time"
20
21 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22
Jan Schär0f8ce4c2025-09-04 13:27:50 +020023 "source.monogon.dev/metropolis/node/allocs"
Lorenz Bruncc078df2021-12-23 11:51:55 +010024 "source.monogon.dev/metropolis/node/core/identity"
25 "source.monogon.dev/metropolis/node/kubernetes/pki"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020026 "source.monogon.dev/osbase/supervisor"
Lorenz Bruncc078df2021-12-23 11:51:55 +010027)
28
29type Service struct {
30 // KPKI is a reference to the Kubernetes PKI
31 KPKI *pki.PKI
Serge Bazanskiad86a552024-01-31 17:46:47 +010032 // Node contains the node credentials
33 Node *identity.NodeCredentials
Lorenz Bruncc078df2021-12-23 11:51:55 +010034}
35
36func (s *Service) getTLSCert(ctx context.Context, name pki.KubeCertificateName) (*tls.Certificate, error) {
37 cert, key, err := s.KPKI.Certificate(ctx, name)
38 if err != nil {
39 return nil, fmt.Errorf("could not load certificate %q from PKI: %w", name, err)
40 }
41 parsedKey, err := x509.ParsePKCS8PrivateKey(key)
42 if err != nil {
43 return nil, fmt.Errorf("failed to parse key for cert %q: %w", name, err)
44 }
45 return &tls.Certificate{
46 Certificate: [][]byte{cert},
47 PrivateKey: parsedKey,
48 }, nil
49}
50
51func respondWithK8sStatus(w http.ResponseWriter, status *metav1.Status) error {
52 w.Header().Set("Content-Type", "application/json")
53 w.WriteHeader(int(status.Code))
54 return json.NewEncoder(w).Encode(status)
55}
56
57func (s *Service) Run(ctx context.Context) error {
58 logger := supervisor.Logger(ctx)
59
60 k8sCAs := x509.NewCertPool()
61 cert, _, err := s.KPKI.Certificate(ctx, pki.IdCA)
Tim Windelschmidt096654a2024-04-18 23:10:19 +020062 if err != nil {
63 return fmt.Errorf("could not load certificate %q from PKI: %w", pki.IdCA, err)
64 }
Lorenz Bruncc078df2021-12-23 11:51:55 +010065 parsedCert, err := x509.ParseCertificate(cert)
66 if err != nil {
67 return fmt.Errorf("failed to parse K8s CA certificate: %w", err)
68 }
69 k8sCAs.AddCert(parsedCert)
70
71 clientCert, err := s.getTLSCert(ctx, pki.MetropolisAuthProxyClient)
72 if err != nil {
73 return err
74 }
75
Jan Schär0f8ce4c2025-09-04 13:27:50 +020076 internalAPIServer := net.JoinHostPort("localhost", allocs.PortKubernetesAPI.PortString())
Lorenz Brun79a1a8f2022-03-31 17:19:07 +020077 standardProxy := httputil.NewSingleHostReverseProxy(&url.URL{
Lorenz Bruncc078df2021-12-23 11:51:55 +010078 Scheme: "https",
Lorenz Brun79a1a8f2022-03-31 17:19:07 +020079 Host: internalAPIServer,
Lorenz Bruncc078df2021-12-23 11:51:55 +010080 })
Lorenz Brun79a1a8f2022-03-31 17:19:07 +020081 noHTTP2Proxy := httputil.NewSingleHostReverseProxy(&url.URL{
82 Scheme: "https",
83 Host: internalAPIServer,
84 })
85 transport := &http.Transport{
Lorenz Bruncc078df2021-12-23 11:51:55 +010086 DialContext: (&net.Dialer{
87 Timeout: 30 * time.Second,
88 KeepAlive: 30 * time.Second,
89 }).DialContext,
90 TLSClientConfig: &tls.Config{
91 RootCAs: k8sCAs,
92 Certificates: []tls.Certificate{*clientCert},
93 NextProtos: []string{"h2", "http/1.1"},
94 },
95 ForceAttemptHTTP2: true,
96 MaxIdleConns: 100,
97 IdleConnTimeout: 90 * time.Second,
98 TLSHandshakeTimeout: 10 * time.Second,
99 ExpectContinueTimeout: 1 * time.Second,
100 }
Lorenz Brun79a1a8f2022-03-31 17:19:07 +0200101 standardProxy.Transport = transport
102 noHTTP2Transport := transport.Clone()
103 noHTTP2Transport.ForceAttemptHTTP2 = false
104 noHTTP2Transport.TLSClientConfig.NextProtos = []string{"http/1.1"}
105 noHTTP2Proxy.Transport = noHTTP2Transport
106 errorHandler := func(w http.ResponseWriter, req *http.Request, err error) {
Lorenz Bruncc078df2021-12-23 11:51:55 +0100107 logger.Infof("Proxy error: %v", err)
108 respondWithK8sStatus(w, &metav1.Status{
109 Status: metav1.StatusFailure,
110 Code: http.StatusBadGateway,
111 Reason: metav1.StatusReasonServiceUnavailable,
112 Message: "authproxy could not reach apiserver",
113 })
114 }
Lorenz Brun79a1a8f2022-03-31 17:19:07 +0200115 standardProxy.ErrorHandler = errorHandler
116 noHTTP2Proxy.ErrorHandler = errorHandler
Lorenz Bruncc078df2021-12-23 11:51:55 +0100117
Serge Bazanskiad86a552024-01-31 17:46:47 +0100118 serverCert := s.Node.TLSCredentials()
Lorenz Bruncc078df2021-12-23 11:51:55 +0100119 clientCAs := x509.NewCertPool()
120 clientCAs.AddCert(s.Node.ClusterCA())
121 server := &http.Server{
Jan Schär0f8ce4c2025-09-04 13:27:50 +0200122 Addr: ":" + allocs.PortKubernetesAPIWrapped.PortString(),
Lorenz Bruncc078df2021-12-23 11:51:55 +0100123 TLSConfig: &tls.Config{
124 MinVersion: tls.VersionTLS12,
125 NextProtos: []string{"h2", "http/1.1"},
126 ClientAuth: tls.RequireAndVerifyClientCert,
127 ClientCAs: clientCAs,
Serge Bazanskiad86a552024-01-31 17:46:47 +0100128 Certificates: []tls.Certificate{serverCert},
Lorenz Bruncc078df2021-12-23 11:51:55 +0100129 },
130 // Limits match @io_k8s_apiserver/pkg/server:secure_serving.go Serve()
131 MaxHeaderBytes: 1 << 20,
132 IdleTimeout: 90 * time.Second,
133 ReadHeaderTimeout: 32 * time.Second,
134
135 Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
136 // Guaranteed to exist because of RequireAndVerifyClientCert
137 clientCert := req.TLS.VerifiedChains[0][0]
138 clientIdentity, err := identity.VerifyUserInCluster(clientCert, s.Node.ClusterCA())
139 if err != nil {
140 respondWithK8sStatus(rw, &metav1.Status{
141 Status: metav1.StatusFailure,
142 Code: http.StatusUnauthorized,
143 Reason: metav1.StatusReasonUnauthorized,
144 Message: fmt.Sprintf("Metropolis authentication failed: %v", err),
145 })
146 return
147 }
Lorenz Brun79a1a8f2022-03-31 17:19:07 +0200148 proxyToUse := standardProxy
149 // Kubernetes wants to use SPDY but using SPDY with HTTP/2 is unsupported.
150 // SPDY should be removed from K8s, this is tracked in
151 // https://github.com/kubernetes/kubernetes/issues/7452
152 if strings.HasPrefix(strings.ToLower(req.Header.Get("Upgrade")), "spdy/") {
153 proxyToUse = noHTTP2Proxy
154 }
Lorenz Bruncc078df2021-12-23 11:51:55 +0100155 // Clone the request as otherwise modifying it is not allowed
156 newReq := req.Clone(req.Context())
157 // Drop any X-Remote headers to prevent injection
158 for k := range newReq.Header {
159 if strings.HasPrefix(http.CanonicalHeaderKey(k), http.CanonicalHeaderKey("X-Remote-")) {
160 newReq.Header.Del(k)
161 }
162 }
163 newReq.Header.Set("X-Remote-User", clientIdentity)
164 newReq.Header.Set("X-Remote-Group", "")
165
Lorenz Brun79a1a8f2022-03-31 17:19:07 +0200166 proxyToUse.ServeHTTP(rw, newReq)
Lorenz Bruncc078df2021-12-23 11:51:55 +0100167 }),
168 }
169 go server.ListenAndServeTLS("", "")
170 logger.Info("K8s AuthProxy running")
171 <-ctx.Done()
172 return server.Close()
173}