proxy.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build !plan9
  4. package main
  5. import (
  6. "crypto/tls"
  7. "fmt"
  8. "log"
  9. "net/http"
  10. "net/http/httputil"
  11. "net/url"
  12. "os"
  13. "strings"
  14. "go.uber.org/zap"
  15. "k8s.io/client-go/rest"
  16. "k8s.io/client-go/transport"
  17. "tailscale.com/client/tailscale"
  18. "tailscale.com/client/tailscale/apitype"
  19. tskube "tailscale.com/kube"
  20. "tailscale.com/tailcfg"
  21. "tailscale.com/tsnet"
  22. "tailscale.com/util/clientmetric"
  23. "tailscale.com/util/ctxkey"
  24. "tailscale.com/util/set"
  25. )
  26. var whoIsKey = ctxkey.New("", (*apitype.WhoIsResponse)(nil))
  27. var counterNumRequestsProxied = clientmetric.NewCounter("k8s_auth_proxy_requests_proxied")
  28. type apiServerProxyMode int
  29. const (
  30. apiserverProxyModeDisabled apiServerProxyMode = iota
  31. apiserverProxyModeEnabled
  32. apiserverProxyModeNoAuth
  33. )
  34. func parseAPIProxyMode() apiServerProxyMode {
  35. haveAuthProxyEnv := os.Getenv("AUTH_PROXY") != ""
  36. haveAPIProxyEnv := os.Getenv("APISERVER_PROXY") != ""
  37. switch {
  38. case haveAPIProxyEnv && haveAuthProxyEnv:
  39. log.Fatal("AUTH_PROXY and APISERVER_PROXY are mutually exclusive")
  40. case haveAuthProxyEnv:
  41. var authProxyEnv = defaultBool("AUTH_PROXY", false) // deprecated
  42. if authProxyEnv {
  43. return apiserverProxyModeEnabled
  44. }
  45. return apiserverProxyModeDisabled
  46. case haveAPIProxyEnv:
  47. var apiProxyEnv = defaultEnv("APISERVER_PROXY", "") // true, false or "noauth"
  48. switch apiProxyEnv {
  49. case "true":
  50. return apiserverProxyModeEnabled
  51. case "false", "":
  52. return apiserverProxyModeDisabled
  53. case "noauth":
  54. return apiserverProxyModeNoAuth
  55. default:
  56. panic(fmt.Sprintf("unknown APISERVER_PROXY value %q", apiProxyEnv))
  57. }
  58. }
  59. return apiserverProxyModeDisabled
  60. }
  61. // maybeLaunchAPIServerProxy launches the auth proxy, which is a small HTTP server
  62. // that authenticates requests using the Tailscale LocalAPI and then proxies
  63. // them to the kube-apiserver.
  64. func maybeLaunchAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, s *tsnet.Server, mode apiServerProxyMode) {
  65. if mode == apiserverProxyModeDisabled {
  66. return
  67. }
  68. startlog := zlog.Named("launchAPIProxy")
  69. if mode == apiserverProxyModeNoAuth {
  70. restConfig = rest.AnonymousClientConfig(restConfig)
  71. }
  72. cfg, err := restConfig.TransportConfig()
  73. if err != nil {
  74. startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
  75. }
  76. // Kubernetes uses SPDY for exec and port-forward, however SPDY is
  77. // incompatible with HTTP/2; so disable HTTP/2 in the proxy.
  78. tr := http.DefaultTransport.(*http.Transport).Clone()
  79. tr.TLSClientConfig, err = transport.TLSConfigFor(cfg)
  80. if err != nil {
  81. startlog.Fatalf("could not get transport.TLSConfigFor(): %v", err)
  82. }
  83. tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
  84. rt, err := transport.HTTPWrappersForConfig(cfg, tr)
  85. if err != nil {
  86. startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
  87. }
  88. go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy"), mode)
  89. }
  90. // apiserverProxy is an http.Handler that authenticates requests using the Tailscale
  91. // LocalAPI and then proxies them to the Kubernetes API.
  92. type apiserverProxy struct {
  93. log *zap.SugaredLogger
  94. lc *tailscale.LocalClient
  95. rp *httputil.ReverseProxy
  96. }
  97. func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  98. who, err := h.lc.WhoIs(r.Context(), r.RemoteAddr)
  99. if err != nil {
  100. h.log.Errorf("failed to authenticate caller: %v", err)
  101. http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
  102. return
  103. }
  104. counterNumRequestsProxied.Add(1)
  105. h.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
  106. }
  107. // runAPIServerProxy runs an HTTP server that authenticates requests using the
  108. // Tailscale LocalAPI and then proxies them to the Kubernetes API.
  109. // It listens on :443 and uses the Tailscale HTTPS certificate.
  110. // s will be started if it is not already running.
  111. // rt is used to proxy requests to the Kubernetes API.
  112. //
  113. // mode controls how the proxy behaves:
  114. // - apiserverProxyModeDisabled: the proxy is not started.
  115. // - apiserverProxyModeEnabled: the proxy is started and requests are impersonated using the
  116. // caller's identity from the Tailscale LocalAPI.
  117. // - apiserverProxyModeNoAuth: the proxy is started and requests are not impersonated and
  118. // are passed through to the Kubernetes API.
  119. //
  120. // It never returns.
  121. func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, log *zap.SugaredLogger, mode apiServerProxyMode) {
  122. if mode == apiserverProxyModeDisabled {
  123. return
  124. }
  125. ln, err := s.Listen("tcp", ":443")
  126. if err != nil {
  127. log.Fatalf("could not listen on :443: %v", err)
  128. }
  129. u, err := url.Parse(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
  130. if err != nil {
  131. log.Fatalf("runAPIServerProxy: failed to parse URL %v", err)
  132. }
  133. lc, err := s.LocalClient()
  134. if err != nil {
  135. log.Fatalf("could not get local client: %v", err)
  136. }
  137. ap := &apiserverProxy{
  138. log: log,
  139. lc: lc,
  140. rp: &httputil.ReverseProxy{
  141. Rewrite: func(r *httputil.ProxyRequest) {
  142. // Replace the URL with the Kubernetes APIServer.
  143. r.Out.URL.Scheme = u.Scheme
  144. r.Out.URL.Host = u.Host
  145. if mode == apiserverProxyModeNoAuth {
  146. // If we are not providing authentication, then we are just
  147. // proxying to the Kubernetes API, so we don't need to do
  148. // anything else.
  149. return
  150. }
  151. // We want to proxy to the Kubernetes API, but we want to use
  152. // the caller's identity to do so. We do this by impersonating
  153. // the caller using the Kubernetes User Impersonation feature:
  154. // https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
  155. // Out of paranoia, remove all authentication headers that might
  156. // have been set by the client.
  157. r.Out.Header.Del("Authorization")
  158. r.Out.Header.Del("Impersonate-Group")
  159. r.Out.Header.Del("Impersonate-User")
  160. r.Out.Header.Del("Impersonate-Uid")
  161. for k := range r.Out.Header {
  162. if strings.HasPrefix(k, "Impersonate-Extra-") {
  163. r.Out.Header.Del(k)
  164. }
  165. }
  166. // Now add the impersonation headers that we want.
  167. if err := addImpersonationHeaders(r.Out, log); err != nil {
  168. panic("failed to add impersonation headers: " + err.Error())
  169. }
  170. },
  171. Transport: rt,
  172. },
  173. }
  174. hs := &http.Server{
  175. // Kubernetes uses SPDY for exec and port-forward, however SPDY is
  176. // incompatible with HTTP/2; so disable HTTP/2 in the proxy.
  177. TLSConfig: &tls.Config{
  178. GetCertificate: lc.GetCertificate,
  179. NextProtos: []string{"http/1.1"},
  180. },
  181. TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
  182. Handler: ap,
  183. }
  184. log.Infof("listening on %s", ln.Addr())
  185. if err := hs.ServeTLS(ln, "", ""); err != nil {
  186. log.Fatalf("runAPIServerProxy: failed to serve %v", err)
  187. }
  188. }
  189. const (
  190. // oldCapabilityName is a legacy form of
  191. // tailfcg.PeerCapabilityKubernetes capability. The only capability rule
  192. // that is respected for this form is group impersonation - for
  193. // backwards compatibility reasons.
  194. // TODO (irbekrm): determine if anyone uses this and remove if possible.
  195. oldCapabilityName = "https://" + tailcfg.PeerCapabilityKubernetes
  196. )
  197. // addImpersonationHeaders adds the appropriate headers to r to impersonate the
  198. // caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed
  199. // in the context by the apiserverProxy.
  200. func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error {
  201. log = log.With("remote", r.RemoteAddr)
  202. who := whoIsKey.Value(r.Context())
  203. rules, err := tailcfg.UnmarshalCapJSON[tskube.KubernetesCapRule](who.CapMap, tailcfg.PeerCapabilityKubernetes)
  204. if len(rules) == 0 && err == nil {
  205. // Try the old capability name for backwards compatibility.
  206. rules, err = tailcfg.UnmarshalCapJSON[tskube.KubernetesCapRule](who.CapMap, oldCapabilityName)
  207. }
  208. if err != nil {
  209. return fmt.Errorf("failed to unmarshal capability: %v", err)
  210. }
  211. var groupsAdded set.Slice[string]
  212. for _, rule := range rules {
  213. if rule.Impersonate == nil {
  214. continue
  215. }
  216. for _, group := range rule.Impersonate.Groups {
  217. if groupsAdded.Contains(group) {
  218. continue
  219. }
  220. r.Header.Add("Impersonate-Group", group)
  221. groupsAdded.Add(group)
  222. log.Debugf("adding group impersonation header for user group %s", group)
  223. }
  224. }
  225. if !who.Node.IsTagged() {
  226. r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
  227. log.Debugf("adding user impersonation header for user %s", who.UserProfile.LoginName)
  228. return nil
  229. }
  230. // "Impersonate-Group" requires "Impersonate-User" to be set, so we set it
  231. // to the node FQDN for tagged nodes.
  232. nodeName := strings.TrimSuffix(who.Node.Name, ".")
  233. r.Header.Set("Impersonate-User", nodeName)
  234. log.Debugf("adding user impersonation header for node name %s", nodeName)
  235. // For legacy behavior (before caps), set the groups to the nodes tags.
  236. if groupsAdded.Slice().Len() == 0 {
  237. for _, tag := range who.Node.Tags {
  238. r.Header.Add("Impersonate-Group", tag)
  239. log.Debugf("adding group impersonation header for node tag %s", tag)
  240. }
  241. }
  242. return nil
  243. }