proxy.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  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/netip"
  12. "net/url"
  13. "os"
  14. "strings"
  15. "github.com/pkg/errors"
  16. "go.uber.org/zap"
  17. "k8s.io/client-go/rest"
  18. "k8s.io/client-go/transport"
  19. "tailscale.com/client/tailscale"
  20. "tailscale.com/client/tailscale/apitype"
  21. ksr "tailscale.com/k8s-operator/sessionrecording"
  22. "tailscale.com/kube/kubetypes"
  23. "tailscale.com/tailcfg"
  24. "tailscale.com/tsnet"
  25. "tailscale.com/util/clientmetric"
  26. "tailscale.com/util/ctxkey"
  27. "tailscale.com/util/set"
  28. )
  29. var (
  30. // counterNumRequestsproxies counts the number of API server requests proxied via this proxy.
  31. counterNumRequestsProxied = clientmetric.NewCounter("k8s_auth_proxy_requests_proxied")
  32. whoIsKey = ctxkey.New("", (*apitype.WhoIsResponse)(nil))
  33. )
  34. type apiServerProxyMode int
  35. func (a apiServerProxyMode) String() string {
  36. switch a {
  37. case apiserverProxyModeDisabled:
  38. return "disabled"
  39. case apiserverProxyModeEnabled:
  40. return "auth"
  41. case apiserverProxyModeNoAuth:
  42. return "noauth"
  43. default:
  44. return "unknown"
  45. }
  46. }
  47. const (
  48. apiserverProxyModeDisabled apiServerProxyMode = iota
  49. apiserverProxyModeEnabled
  50. apiserverProxyModeNoAuth
  51. )
  52. func parseAPIProxyMode() apiServerProxyMode {
  53. haveAuthProxyEnv := os.Getenv("AUTH_PROXY") != ""
  54. haveAPIProxyEnv := os.Getenv("APISERVER_PROXY") != ""
  55. switch {
  56. case haveAPIProxyEnv && haveAuthProxyEnv:
  57. log.Fatal("AUTH_PROXY and APISERVER_PROXY are mutually exclusive")
  58. case haveAuthProxyEnv:
  59. var authProxyEnv = defaultBool("AUTH_PROXY", false) // deprecated
  60. if authProxyEnv {
  61. return apiserverProxyModeEnabled
  62. }
  63. return apiserverProxyModeDisabled
  64. case haveAPIProxyEnv:
  65. var apiProxyEnv = defaultEnv("APISERVER_PROXY", "") // true, false or "noauth"
  66. switch apiProxyEnv {
  67. case "true":
  68. return apiserverProxyModeEnabled
  69. case "false", "":
  70. return apiserverProxyModeDisabled
  71. case "noauth":
  72. return apiserverProxyModeNoAuth
  73. default:
  74. panic(fmt.Sprintf("unknown APISERVER_PROXY value %q", apiProxyEnv))
  75. }
  76. }
  77. return apiserverProxyModeDisabled
  78. }
  79. // maybeLaunchAPIServerProxy launches the auth proxy, which is a small HTTP server
  80. // that authenticates requests using the Tailscale LocalAPI and then proxies
  81. // them to the kube-apiserver.
  82. func maybeLaunchAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, s *tsnet.Server, mode apiServerProxyMode) {
  83. if mode == apiserverProxyModeDisabled {
  84. return
  85. }
  86. startlog := zlog.Named("launchAPIProxy")
  87. if mode == apiserverProxyModeNoAuth {
  88. restConfig = rest.AnonymousClientConfig(restConfig)
  89. }
  90. cfg, err := restConfig.TransportConfig()
  91. if err != nil {
  92. startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
  93. }
  94. // Kubernetes uses SPDY for exec and port-forward, however SPDY is
  95. // incompatible with HTTP/2; so disable HTTP/2 in the proxy.
  96. tr := http.DefaultTransport.(*http.Transport).Clone()
  97. tr.TLSClientConfig, err = transport.TLSConfigFor(cfg)
  98. if err != nil {
  99. startlog.Fatalf("could not get transport.TLSConfigFor(): %v", err)
  100. }
  101. tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
  102. rt, err := transport.HTTPWrappersForConfig(cfg, tr)
  103. if err != nil {
  104. startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
  105. }
  106. go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy"), mode, restConfig.Host)
  107. }
  108. // runAPIServerProxy runs an HTTP server that authenticates requests using the
  109. // Tailscale LocalAPI and then proxies them to the Kubernetes API.
  110. // It listens on :443 and uses the Tailscale HTTPS certificate.
  111. // s will be started if it is not already running.
  112. // rt is used to proxy requests to the Kubernetes API.
  113. //
  114. // mode controls how the proxy behaves:
  115. // - apiserverProxyModeDisabled: the proxy is not started.
  116. // - apiserverProxyModeEnabled: the proxy is started and requests are impersonated using the
  117. // caller's identity from the Tailscale LocalAPI.
  118. // - apiserverProxyModeNoAuth: the proxy is started and requests are not impersonated and
  119. // are passed through to the Kubernetes API.
  120. //
  121. // It never returns.
  122. func runAPIServerProxy(ts *tsnet.Server, rt http.RoundTripper, log *zap.SugaredLogger, mode apiServerProxyMode, host string) {
  123. if mode == apiserverProxyModeDisabled {
  124. return
  125. }
  126. ln, err := ts.Listen("tcp", ":443")
  127. if err != nil {
  128. log.Fatalf("could not listen on :443: %v", err)
  129. }
  130. u, err := url.Parse(host)
  131. if err != nil {
  132. log.Fatalf("runAPIServerProxy: failed to parse URL %v", err)
  133. }
  134. lc, err := ts.LocalClient()
  135. if err != nil {
  136. log.Fatalf("could not get local client: %v", err)
  137. }
  138. ap := &apiserverProxy{
  139. log: log,
  140. lc: lc,
  141. mode: mode,
  142. upstreamURL: u,
  143. ts: ts,
  144. }
  145. ap.rp = &httputil.ReverseProxy{
  146. Rewrite: func(pr *httputil.ProxyRequest) {
  147. ap.addImpersonationHeadersAsRequired(pr.Out)
  148. },
  149. Transport: rt,
  150. }
  151. mux := http.NewServeMux()
  152. mux.HandleFunc("/", ap.serveDefault)
  153. mux.HandleFunc("POST /api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExecSPDY)
  154. mux.HandleFunc("GET /api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExecWS)
  155. hs := &http.Server{
  156. // Kubernetes uses SPDY for exec and port-forward, however SPDY is
  157. // incompatible with HTTP/2; so disable HTTP/2 in the proxy.
  158. TLSConfig: &tls.Config{
  159. GetCertificate: lc.GetCertificate,
  160. NextProtos: []string{"http/1.1"},
  161. },
  162. TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
  163. Handler: mux,
  164. }
  165. log.Infof("API server proxy in %q mode is listening on %s", mode, ln.Addr())
  166. if err := hs.ServeTLS(ln, "", ""); err != nil {
  167. log.Fatalf("runAPIServerProxy: failed to serve %v", err)
  168. }
  169. }
  170. // apiserverProxy is an [net/http.Handler] that authenticates requests using the Tailscale
  171. // LocalAPI and then proxies them to the Kubernetes API.
  172. type apiserverProxy struct {
  173. log *zap.SugaredLogger
  174. lc *tailscale.LocalClient
  175. rp *httputil.ReverseProxy
  176. mode apiServerProxyMode
  177. ts *tsnet.Server
  178. upstreamURL *url.URL
  179. }
  180. // serveDefault is the default handler for Kubernetes API server requests.
  181. func (ap *apiserverProxy) serveDefault(w http.ResponseWriter, r *http.Request) {
  182. who, err := ap.whoIs(r)
  183. if err != nil {
  184. ap.authError(w, err)
  185. return
  186. }
  187. counterNumRequestsProxied.Add(1)
  188. ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
  189. }
  190. // serveExecSPDY serves 'kubectl exec' requests for sessions streamed over SPDY,
  191. // optionally configuring the kubectl exec sessions to be recorded.
  192. func (ap *apiserverProxy) serveExecSPDY(w http.ResponseWriter, r *http.Request) {
  193. ap.execForProto(w, r, ksr.SPDYProtocol)
  194. }
  195. // serveExecWS serves 'kubectl exec' requests for sessions streamed over WebSocket,
  196. // optionally configuring the kubectl exec sessions to be recorded.
  197. func (ap *apiserverProxy) serveExecWS(w http.ResponseWriter, r *http.Request) {
  198. ap.execForProto(w, r, ksr.WSProtocol)
  199. }
  200. func (ap *apiserverProxy) execForProto(w http.ResponseWriter, r *http.Request, proto ksr.Protocol) {
  201. const (
  202. podNameKey = "pod"
  203. namespaceNameKey = "namespace"
  204. upgradeHeaderKey = "Upgrade"
  205. )
  206. who, err := ap.whoIs(r)
  207. if err != nil {
  208. ap.authError(w, err)
  209. return
  210. }
  211. counterNumRequestsProxied.Add(1)
  212. failOpen, addrs, err := determineRecorderConfig(who)
  213. if err != nil {
  214. ap.log.Errorf("error trying to determine whether the 'kubectl exec' session needs to be recorded: %v", err)
  215. return
  216. }
  217. if failOpen && len(addrs) == 0 { // will not record
  218. ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
  219. return
  220. }
  221. ksr.CounterSessionRecordingsAttempted.Add(1) // at this point we know that users intended for this session to be recorded
  222. if !failOpen && len(addrs) == 0 {
  223. msg := "forbidden: 'kubectl exec' session must be recorded, but no recorders are available."
  224. ap.log.Error(msg)
  225. http.Error(w, msg, http.StatusForbidden)
  226. return
  227. }
  228. wantsHeader := upgradeHeaderForProto[proto]
  229. if h := r.Header.Get(upgradeHeaderKey); h != wantsHeader {
  230. msg := fmt.Sprintf("[unexpected] unable to verify that streaming protocol is %s, wants Upgrade header %q, got: %q", proto, wantsHeader, h)
  231. if failOpen {
  232. msg = msg + "; failure mode is 'fail open'; continuing session without recording."
  233. ap.log.Warn(msg)
  234. ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
  235. return
  236. }
  237. ap.log.Error(msg)
  238. msg += "; failure mode is 'fail closed'; closing connection."
  239. http.Error(w, msg, http.StatusForbidden)
  240. return
  241. }
  242. opts := ksr.HijackerOpts{
  243. Req: r,
  244. W: w,
  245. Proto: proto,
  246. TS: ap.ts,
  247. Who: who,
  248. Addrs: addrs,
  249. FailOpen: failOpen,
  250. Pod: r.PathValue(podNameKey),
  251. Namespace: r.PathValue(namespaceNameKey),
  252. Log: ap.log,
  253. }
  254. h := ksr.New(opts)
  255. ap.rp.ServeHTTP(h, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
  256. }
  257. func (h *apiserverProxy) addImpersonationHeadersAsRequired(r *http.Request) {
  258. r.URL.Scheme = h.upstreamURL.Scheme
  259. r.URL.Host = h.upstreamURL.Host
  260. if h.mode == apiserverProxyModeNoAuth {
  261. // If we are not providing authentication, then we are just
  262. // proxying to the Kubernetes API, so we don't need to do
  263. // anything else.
  264. return
  265. }
  266. // We want to proxy to the Kubernetes API, but we want to use
  267. // the caller's identity to do so. We do this by impersonating
  268. // the caller using the Kubernetes User Impersonation feature:
  269. // https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
  270. // Out of paranoia, remove all authentication headers that might
  271. // have been set by the client.
  272. r.Header.Del("Authorization")
  273. r.Header.Del("Impersonate-Group")
  274. r.Header.Del("Impersonate-User")
  275. r.Header.Del("Impersonate-Uid")
  276. for k := range r.Header {
  277. if strings.HasPrefix(k, "Impersonate-Extra-") {
  278. r.Header.Del(k)
  279. }
  280. }
  281. // Now add the impersonation headers that we want.
  282. if err := addImpersonationHeaders(r, h.log); err != nil {
  283. log.Printf("failed to add impersonation headers: " + err.Error())
  284. }
  285. }
  286. func (ap *apiserverProxy) whoIs(r *http.Request) (*apitype.WhoIsResponse, error) {
  287. return ap.lc.WhoIs(r.Context(), r.RemoteAddr)
  288. }
  289. func (ap *apiserverProxy) authError(w http.ResponseWriter, err error) {
  290. ap.log.Errorf("failed to authenticate caller: %v", err)
  291. http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
  292. }
  293. const (
  294. // oldCapabilityName is a legacy form of
  295. // tailfcg.PeerCapabilityKubernetes capability. The only capability rule
  296. // that is respected for this form is group impersonation - for
  297. // backwards compatibility reasons.
  298. // TODO (irbekrm): determine if anyone uses this and remove if possible.
  299. oldCapabilityName = "https://" + tailcfg.PeerCapabilityKubernetes
  300. )
  301. // addImpersonationHeaders adds the appropriate headers to r to impersonate the
  302. // caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed
  303. // in the context by the apiserverProxy.
  304. func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error {
  305. log = log.With("remote", r.RemoteAddr)
  306. who := whoIsKey.Value(r.Context())
  307. rules, err := tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, tailcfg.PeerCapabilityKubernetes)
  308. if len(rules) == 0 && err == nil {
  309. // Try the old capability name for backwards compatibility.
  310. rules, err = tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, oldCapabilityName)
  311. }
  312. if err != nil {
  313. return fmt.Errorf("failed to unmarshal capability: %v", err)
  314. }
  315. var groupsAdded set.Slice[string]
  316. for _, rule := range rules {
  317. if rule.Impersonate == nil {
  318. continue
  319. }
  320. for _, group := range rule.Impersonate.Groups {
  321. if groupsAdded.Contains(group) {
  322. continue
  323. }
  324. r.Header.Add("Impersonate-Group", group)
  325. groupsAdded.Add(group)
  326. log.Debugf("adding group impersonation header for user group %s", group)
  327. }
  328. }
  329. if !who.Node.IsTagged() {
  330. r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
  331. log.Debugf("adding user impersonation header for user %s", who.UserProfile.LoginName)
  332. return nil
  333. }
  334. // "Impersonate-Group" requires "Impersonate-User" to be set, so we set it
  335. // to the node FQDN for tagged nodes.
  336. nodeName := strings.TrimSuffix(who.Node.Name, ".")
  337. r.Header.Set("Impersonate-User", nodeName)
  338. log.Debugf("adding user impersonation header for node name %s", nodeName)
  339. // For legacy behavior (before caps), set the groups to the nodes tags.
  340. if groupsAdded.Slice().Len() == 0 {
  341. for _, tag := range who.Node.Tags {
  342. r.Header.Add("Impersonate-Group", tag)
  343. log.Debugf("adding group impersonation header for node tag %s", tag)
  344. }
  345. }
  346. return nil
  347. }
  348. // determineRecorderConfig determines recorder config from requester's peer
  349. // capabilities. Determines whether a 'kubectl exec' session from this requester
  350. // needs to be recorded and what recorders the recording should be sent to.
  351. func determineRecorderConfig(who *apitype.WhoIsResponse) (failOpen bool, recorderAddresses []netip.AddrPort, _ error) {
  352. if who == nil {
  353. return false, nil, errors.New("[unexpected] cannot determine caller")
  354. }
  355. failOpen = true
  356. rules, err := tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, tailcfg.PeerCapabilityKubernetes)
  357. if err != nil {
  358. return failOpen, nil, fmt.Errorf("failed to unmarshal Kubernetes capability: %w", err)
  359. }
  360. if len(rules) == 0 {
  361. return failOpen, nil, nil
  362. }
  363. for _, rule := range rules {
  364. if len(rule.RecorderAddrs) != 0 {
  365. // TODO (irbekrm): here or later determine if the
  366. // recorders behind those addrs are online - else we
  367. // spend 30s trying to reach a recorder whose tailscale
  368. // status is offline.
  369. recorderAddresses = append(recorderAddresses, rule.RecorderAddrs...)
  370. }
  371. if rule.EnforceRecorder {
  372. failOpen = false
  373. }
  374. }
  375. return failOpen, recorderAddresses, nil
  376. }
  377. var upgradeHeaderForProto = map[ksr.Protocol]string{
  378. ksr.SPDYProtocol: "SPDY/3.1",
  379. ksr.WSProtocol: "websocket",
  380. }