From dd8bc9ba0379984f8e9111944f920886f609310c Mon Sep 17 00:00:00 2001 From: Irbe Krumina Date: Mon, 20 Nov 2023 16:41:18 +0100 Subject: [PATCH] cmd/k8s-operator: log user/group impersonated by apiserver proxy (#10334) Updates tailscale/tailscale#10127 Signed-off-by: Irbe Krumina --- cmd/k8s-operator/proxy.go | 30 ++++++++++++++++++------------ cmd/k8s-operator/proxy_test.go | 7 ++++++- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/cmd/k8s-operator/proxy.go b/cmd/k8s-operator/proxy.go index da9cf5bfa..6de1f71e2 100644 --- a/cmd/k8s-operator/proxy.go +++ b/cmd/k8s-operator/proxy.go @@ -23,7 +23,6 @@ import ( "tailscale.com/client/tailscale/apitype" "tailscale.com/tailcfg" "tailscale.com/tsnet" - "tailscale.com/types/logger" "tailscale.com/util/clientmetric" "tailscale.com/util/set" ) @@ -109,21 +108,21 @@ func maybeLaunchAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, if err != nil { startlog.Fatalf("could not get rest.TransportConfig(): %v", err) } - go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy").Infof, mode) + go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy"), mode) } // apiserverProxy is an http.Handler that authenticates requests using the Tailscale // LocalAPI and then proxies them to the Kubernetes API. type apiserverProxy struct { - logf logger.Logf - lc *tailscale.LocalClient - rp *httputil.ReverseProxy + log *zap.SugaredLogger + lc *tailscale.LocalClient + rp *httputil.ReverseProxy } func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { who, err := h.lc.WhoIs(r.Context(), r.RemoteAddr) if err != nil { - h.logf("failed to authenticate caller: %v", err) + h.log.Errorf("failed to authenticate caller: %v", err) http.Error(w, "failed to authenticate caller", http.StatusInternalServerError) return } @@ -145,7 +144,7 @@ func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // are passed through to the Kubernetes API. // // It never returns. -func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf, mode apiServerProxyMode) { +func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, log *zap.SugaredLogger, mode apiServerProxyMode) { if mode == apiserverProxyModeDisabled { return } @@ -163,8 +162,8 @@ func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf, log.Fatalf("could not get local client: %v", err) } ap := &apiserverProxy{ - logf: logf, - lc: lc, + log: log, + lc: lc, rp: &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { // Replace the URL with the Kubernetes APIServer. @@ -196,7 +195,7 @@ func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf, } // Now add the impersonation headers that we want. - if err := addImpersonationHeaders(r.Out); err != nil { + if err := addImpersonationHeaders(r.Out, log); err != nil { panic("failed to add impersonation headers: " + err.Error()) } }, @@ -213,6 +212,7 @@ func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf, TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), Handler: ap, } + log.Infof("listening on %s", ln.Addr()) if err := hs.ServeTLS(ln, "", ""); err != nil { log.Fatalf("runAPIServerProxy: failed to serve %v", err) } @@ -235,7 +235,8 @@ type impersonateRule struct { // addImpersonationHeaders adds the appropriate headers to r to impersonate the // caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed // in the context by the apiserverProxy. -func addImpersonationHeaders(r *http.Request) error { +func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error { + log = log.With("remote", r.RemoteAddr) who := whoIsFromRequest(r) rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, capabilityName) if err != nil { @@ -253,21 +254,26 @@ func addImpersonationHeaders(r *http.Request) error { } r.Header.Add("Impersonate-Group", group) groupsAdded.Add(group) + log.Debugf("adding group impersonation header for user group %s", group) } } if !who.Node.IsTagged() { r.Header.Set("Impersonate-User", who.UserProfile.LoginName) + log.Debugf("adding user impersonation header for user %s", who.UserProfile.LoginName) return nil } // "Impersonate-Group" requires "Impersonate-User" to be set, so we set it // to the node FQDN for tagged nodes. - r.Header.Set("Impersonate-User", strings.TrimSuffix(who.Node.Name, ".")) + nodeName := strings.TrimSuffix(who.Node.Name, ".") + r.Header.Set("Impersonate-User", nodeName) + log.Debugf("adding user impersonation header for node name %s", nodeName) // For legacy behavior (before caps), set the groups to the nodes tags. if groupsAdded.Slice().Len() == 0 { for _, tag := range who.Node.Tags { r.Header.Add("Impersonate-Group", tag) + log.Debugf("adding group impersonation header for node tag %s", tag) } } return nil diff --git a/cmd/k8s-operator/proxy_test.go b/cmd/k8s-operator/proxy_test.go index 9940e80c9..a1f1835f1 100644 --- a/cmd/k8s-operator/proxy_test.go +++ b/cmd/k8s-operator/proxy_test.go @@ -10,12 +10,17 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "go.uber.org/zap" "tailscale.com/client/tailscale/apitype" "tailscale.com/tailcfg" "tailscale.com/util/must" ) func TestImpersonationHeaders(t *testing.T) { + zl, err := zap.NewDevelopment() + if err != nil { + t.Fatal(err) + } tests := []struct { name string emailish string @@ -100,7 +105,7 @@ func TestImpersonationHeaders(t *testing.T) { }, CapMap: tc.capMap, }) - addImpersonationHeaders(r) + addImpersonationHeaders(r, zl.Sugar()) if d := cmp.Diff(tc.wantHeaders, r.Header); d != "" { t.Errorf("unexpected header (-want +got):\n%s", d)