blob: 0477b88858aea634f5bbc1c37641ce98d7909ca6 [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
70 proxy := httputil.NewSingleHostReverseProxy(&url.URL{
71 Host: net.JoinHostPort("localhost", node.KubernetesAPIPort.PortString()),
72 Scheme: "https",
73 })
74 proxy.Transport = &http.Transport{
75 DialContext: (&net.Dialer{
76 Timeout: 30 * time.Second,
77 KeepAlive: 30 * time.Second,
78 }).DialContext,
79 TLSClientConfig: &tls.Config{
80 RootCAs: k8sCAs,
81 Certificates: []tls.Certificate{*clientCert},
82 NextProtos: []string{"h2", "http/1.1"},
83 },
84 ForceAttemptHTTP2: true,
85 MaxIdleConns: 100,
86 IdleConnTimeout: 90 * time.Second,
87 TLSHandshakeTimeout: 10 * time.Second,
88 ExpectContinueTimeout: 1 * time.Second,
89 }
90 proxy.ErrorHandler = func(w http.ResponseWriter, req *http.Request, err error) {
91 logger.Infof("Proxy error: %v", err)
92 respondWithK8sStatus(w, &metav1.Status{
93 Status: metav1.StatusFailure,
94 Code: http.StatusBadGateway,
95 Reason: metav1.StatusReasonServiceUnavailable,
96 Message: "authproxy could not reach apiserver",
97 })
98 }
99
100 serverCert, err := s.getTLSCert(ctx, pki.APIServer)
101 if err != nil {
102 return err
103 }
104 clientCAs := x509.NewCertPool()
105 clientCAs.AddCert(s.Node.ClusterCA())
106 server := &http.Server{
107 Addr: ":" + node.KubernetesAPIWrappedPort.PortString(),
108 TLSConfig: &tls.Config{
109 MinVersion: tls.VersionTLS12,
110 NextProtos: []string{"h2", "http/1.1"},
111 ClientAuth: tls.RequireAndVerifyClientCert,
112 ClientCAs: clientCAs,
113 Certificates: []tls.Certificate{*serverCert},
114 },
115 // Limits match @io_k8s_apiserver/pkg/server:secure_serving.go Serve()
116 MaxHeaderBytes: 1 << 20,
117 IdleTimeout: 90 * time.Second,
118 ReadHeaderTimeout: 32 * time.Second,
119
120 Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
121 // Guaranteed to exist because of RequireAndVerifyClientCert
122 clientCert := req.TLS.VerifiedChains[0][0]
123 clientIdentity, err := identity.VerifyUserInCluster(clientCert, s.Node.ClusterCA())
124 if err != nil {
125 respondWithK8sStatus(rw, &metav1.Status{
126 Status: metav1.StatusFailure,
127 Code: http.StatusUnauthorized,
128 Reason: metav1.StatusReasonUnauthorized,
129 Message: fmt.Sprintf("Metropolis authentication failed: %v", err),
130 })
131 return
132 }
133 // Clone the request as otherwise modifying it is not allowed
134 newReq := req.Clone(req.Context())
135 // Drop any X-Remote headers to prevent injection
136 for k := range newReq.Header {
137 if strings.HasPrefix(http.CanonicalHeaderKey(k), http.CanonicalHeaderKey("X-Remote-")) {
138 newReq.Header.Del(k)
139 }
140 }
141 newReq.Header.Set("X-Remote-User", clientIdentity)
142 newReq.Header.Set("X-Remote-Group", "")
143
144 proxy.ServeHTTP(rw, newReq)
145 }),
146 }
147 go server.ListenAndServeTLS("", "")
148 logger.Info("K8s AuthProxy running")
149 <-ctx.Done()
150 return server.Close()
151}