cmd/k8s-operator: log user/group impersonated by apiserver proxy (#10334)

Updates tailscale/tailscale#10127

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
pull/10342/head
Irbe Krumina 7 months ago committed by GitHub
parent 4f80f403be
commit dd8bc9ba03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -23,7 +23,6 @@ import (
"tailscale.com/client/tailscale/apitype" "tailscale.com/client/tailscale/apitype"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/tsnet" "tailscale.com/tsnet"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/util/set" "tailscale.com/util/set"
) )
@ -109,21 +108,21 @@ func maybeLaunchAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config,
if err != nil { if err != nil {
startlog.Fatalf("could not get rest.TransportConfig(): %v", err) 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 // apiserverProxy is an http.Handler that authenticates requests using the Tailscale
// LocalAPI and then proxies them to the Kubernetes API. // LocalAPI and then proxies them to the Kubernetes API.
type apiserverProxy struct { type apiserverProxy struct {
logf logger.Logf log *zap.SugaredLogger
lc *tailscale.LocalClient lc *tailscale.LocalClient
rp *httputil.ReverseProxy rp *httputil.ReverseProxy
} }
func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
who, err := h.lc.WhoIs(r.Context(), r.RemoteAddr) who, err := h.lc.WhoIs(r.Context(), r.RemoteAddr)
if err != nil { 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) http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
return return
} }
@ -145,7 +144,7 @@ func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// are passed through to the Kubernetes API. // are passed through to the Kubernetes API.
// //
// It never returns. // 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 { if mode == apiserverProxyModeDisabled {
return 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) log.Fatalf("could not get local client: %v", err)
} }
ap := &apiserverProxy{ ap := &apiserverProxy{
logf: logf, log: log,
lc: lc, lc: lc,
rp: &httputil.ReverseProxy{ rp: &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) { Rewrite: func(r *httputil.ProxyRequest) {
// Replace the URL with the Kubernetes APIServer. // 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. // 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()) 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)), TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
Handler: ap, Handler: ap,
} }
log.Infof("listening on %s", ln.Addr())
if err := hs.ServeTLS(ln, "", ""); err != nil { if err := hs.ServeTLS(ln, "", ""); err != nil {
log.Fatalf("runAPIServerProxy: failed to serve %v", err) 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 // addImpersonationHeaders adds the appropriate headers to r to impersonate the
// caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed // caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed
// in the context by the apiserverProxy. // 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) who := whoIsFromRequest(r)
rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, capabilityName) rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, capabilityName)
if err != nil { if err != nil {
@ -253,21 +254,26 @@ func addImpersonationHeaders(r *http.Request) error {
} }
r.Header.Add("Impersonate-Group", group) r.Header.Add("Impersonate-Group", group)
groupsAdded.Add(group) groupsAdded.Add(group)
log.Debugf("adding group impersonation header for user group %s", group)
} }
} }
if !who.Node.IsTagged() { if !who.Node.IsTagged() {
r.Header.Set("Impersonate-User", who.UserProfile.LoginName) r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
log.Debugf("adding user impersonation header for user %s", who.UserProfile.LoginName)
return nil return nil
} }
// "Impersonate-Group" requires "Impersonate-User" to be set, so we set it // "Impersonate-Group" requires "Impersonate-User" to be set, so we set it
// to the node FQDN for tagged nodes. // 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. // For legacy behavior (before caps), set the groups to the nodes tags.
if groupsAdded.Slice().Len() == 0 { if groupsAdded.Slice().Len() == 0 {
for _, tag := range who.Node.Tags { for _, tag := range who.Node.Tags {
r.Header.Add("Impersonate-Group", tag) r.Header.Add("Impersonate-Group", tag)
log.Debugf("adding group impersonation header for node tag %s", tag)
} }
} }
return nil return nil

@ -10,12 +10,17 @@ import (
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"go.uber.org/zap"
"tailscale.com/client/tailscale/apitype" "tailscale.com/client/tailscale/apitype"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/util/must" "tailscale.com/util/must"
) )
func TestImpersonationHeaders(t *testing.T) { func TestImpersonationHeaders(t *testing.T) {
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
tests := []struct { tests := []struct {
name string name string
emailish string emailish string
@ -100,7 +105,7 @@ func TestImpersonationHeaders(t *testing.T) {
}, },
CapMap: tc.capMap, CapMap: tc.capMap,
}) })
addImpersonationHeaders(r) addImpersonationHeaders(r, zl.Sugar())
if d := cmp.Diff(tc.wantHeaders, r.Header); d != "" { if d := cmp.Diff(tc.wantHeaders, r.Header); d != "" {
t.Errorf("unexpected header (-want +got):\n%s", d) t.Errorf("unexpected header (-want +got):\n%s", d)

Loading…
Cancel
Save