From 22a8e0ac50ee2211e013fae2f2dbd8a9622657d8 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Tue, 22 Jul 2025 14:46:38 +0100 Subject: [PATCH] cmd/{k8s-operator,k8s-proxy},kube: use consistent type for auth mode config (#16626) Updates k8s-proxy's config so its auth mode config matches that we set in kube-apiserver ProxyGroups for consistency. Updates #13358 Change-Id: I95e29cec6ded2dc7c6d2d03f968a25c822bc0e01 Signed-off-by: Tom Proctor --- cmd/k8s-operator/api-server-proxy.go | 38 ++++++-------------- cmd/k8s-operator/operator.go | 6 ++-- cmd/k8s-operator/proxygroup.go | 8 +++-- cmd/k8s-operator/proxygroup_test.go | 2 +- cmd/k8s-proxy/k8s-proxy.go | 9 ++--- k8s-operator/api-proxy/proxy.go | 8 ++--- k8s-operator/sessionrecording/hijacker.go | 2 +- kube/k8s-proxy/conf/conf.go | 9 ++--- kube/kubetypes/types.go | 23 ++++++++++++- kube/kubetypes/types_test.go | 42 +++++++++++++++++++++++ 10 files changed, 99 insertions(+), 48 deletions(-) create mode 100644 kube/kubetypes/types_test.go diff --git a/cmd/k8s-operator/api-server-proxy.go b/cmd/k8s-operator/api-server-proxy.go index 09a7b8c62..70333d2c4 100644 --- a/cmd/k8s-operator/api-server-proxy.go +++ b/cmd/k8s-operator/api-server-proxy.go @@ -9,30 +9,12 @@ import ( "fmt" "log" "os" + + "tailscale.com/kube/kubetypes" + "tailscale.com/types/ptr" ) -type apiServerProxyMode int - -func (a apiServerProxyMode) String() string { - switch a { - case apiServerProxyModeDisabled: - return "disabled" - case apiServerProxyModeEnabled: - return "auth" - case apiServerProxyModeNoAuth: - return "noauth" - default: - return "unknown" - } -} - -const ( - apiServerProxyModeDisabled apiServerProxyMode = iota - apiServerProxyModeEnabled - apiServerProxyModeNoAuth -) - -func parseAPIProxyMode() apiServerProxyMode { +func parseAPIProxyMode() *kubetypes.APIServerProxyMode { haveAuthProxyEnv := os.Getenv("AUTH_PROXY") != "" haveAPIProxyEnv := os.Getenv("APISERVER_PROXY") != "" switch { @@ -41,21 +23,21 @@ func parseAPIProxyMode() apiServerProxyMode { case haveAuthProxyEnv: var authProxyEnv = defaultBool("AUTH_PROXY", false) // deprecated if authProxyEnv { - return apiServerProxyModeEnabled + return ptr.To(kubetypes.APIServerProxyModeAuth) } - return apiServerProxyModeDisabled + return nil case haveAPIProxyEnv: var apiProxyEnv = defaultEnv("APISERVER_PROXY", "") // true, false or "noauth" switch apiProxyEnv { case "true": - return apiServerProxyModeEnabled + return ptr.To(kubetypes.APIServerProxyModeAuth) case "false", "": - return apiServerProxyModeDisabled + return nil case "noauth": - return apiServerProxyModeNoAuth + return ptr.To(kubetypes.APIServerProxyModeNoAuth) default: panic(fmt.Sprintf("unknown APISERVER_PROXY value %q", apiProxyEnv)) } } - return apiServerProxyModeDisabled + return nil } diff --git a/cmd/k8s-operator/operator.go b/cmd/k8s-operator/operator.go index 94a0a6a78..76d2df51d 100644 --- a/cmd/k8s-operator/operator.go +++ b/cmd/k8s-operator/operator.go @@ -113,7 +113,7 @@ func main() { // additionally act as api-server proxy // https://tailscale.com/kb/1236/kubernetes-operator/?q=kubernetes#accessing-the-kubernetes-control-plane-using-an-api-server-proxy. mode := parseAPIProxyMode() - if mode == apiServerProxyModeDisabled { + if mode == nil { hostinfo.SetApp(kubetypes.AppOperator) } else { hostinfo.SetApp(kubetypes.AppInProcessAPIServerProxy) @@ -122,8 +122,8 @@ func main() { s, tsc := initTSNet(zlog, loginServer) defer s.Close() restConfig := config.GetConfigOrDie() - if mode != apiServerProxyModeDisabled { - ap, err := apiproxy.NewAPIServerProxy(zlog, restConfig, s, mode == apiServerProxyModeEnabled, true) + if mode != nil { + ap, err := apiproxy.NewAPIServerProxy(zlog, restConfig, s, *mode, true) if err != nil { zlog.Fatalf("error creating API server proxy: %v", err) } diff --git a/cmd/k8s-operator/proxygroup.go b/cmd/k8s-operator/proxygroup.go index d62cb0f11..f9c12797d 100644 --- a/cmd/k8s-operator/proxygroup.go +++ b/cmd/k8s-operator/proxygroup.go @@ -805,6 +805,10 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p } } + mode := kubetypes.APIServerProxyModeAuth + if !isAuthAPIServerProxy(pg) { + mode = kubetypes.APIServerProxyModeNoAuth + } cfg := conf.VersionedConfig{ Version: "v1alpha1", ConfigV1Alpha1: &conf.ConfigV1Alpha1{ @@ -816,8 +820,8 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p // Reloadable fields. Hostname: &hostname, APIServerProxy: &conf.APIServerProxyConfig{ - Enabled: opt.NewBool(true), - AuthMode: opt.NewBool(isAuthAPIServerProxy(pg)), + Enabled: opt.NewBool(true), + Mode: &mode, // The first replica is elected as the cert issuer, same // as containerboot does for ingress-pg-reconciler. IssueCerts: opt.NewBool(i == 0), diff --git a/cmd/k8s-operator/proxygroup_test.go b/cmd/k8s-operator/proxygroup_test.go index ef6babc56..0dc791b04 100644 --- a/cmd/k8s-operator/proxygroup_test.go +++ b/cmd/k8s-operator/proxygroup_test.go @@ -1376,7 +1376,7 @@ func TestKubeAPIServerType_DoesNotOverwriteServicesConfig(t *testing.T) { Hostname: ptr.To("test-k8s-apiserver-0"), APIServerProxy: &conf.APIServerProxyConfig{ Enabled: opt.NewBool(true), - AuthMode: opt.NewBool(false), + Mode: ptr.To(kubetypes.APIServerProxyModeNoAuth), IssueCerts: opt.NewBool(true), }, }, diff --git a/cmd/k8s-proxy/k8s-proxy.go b/cmd/k8s-proxy/k8s-proxy.go index eea1f15f7..b56ceaab0 100644 --- a/cmd/k8s-proxy/k8s-proxy.go +++ b/cmd/k8s-proxy/k8s-proxy.go @@ -34,6 +34,7 @@ import ( apiproxy "tailscale.com/k8s-operator/api-proxy" "tailscale.com/kube/certs" "tailscale.com/kube/k8s-proxy/conf" + "tailscale.com/kube/kubetypes" klc "tailscale.com/kube/localclient" "tailscale.com/kube/services" "tailscale.com/kube/state" @@ -238,11 +239,11 @@ func run(logger *zap.SugaredLogger) error { } // Setup for the API server proxy. - authMode := true - if cfg.Parsed.APIServerProxy != nil && cfg.Parsed.APIServerProxy.AuthMode.EqualBool(false) { - authMode = false + mode := kubetypes.APIServerProxyModeAuth + if cfg.Parsed.APIServerProxy != nil && cfg.Parsed.APIServerProxy.Mode != nil { + mode = *cfg.Parsed.APIServerProxy.Mode } - ap, err := apiproxy.NewAPIServerProxy(logger.Named("apiserver-proxy"), restConfig, ts, authMode, false) + ap, err := apiproxy.NewAPIServerProxy(logger.Named("apiserver-proxy"), restConfig, ts, mode, false) if err != nil { return fmt.Errorf("error creating api server proxy: %w", err) } diff --git a/k8s-operator/api-proxy/proxy.go b/k8s-operator/api-proxy/proxy.go index e079e984f..c648e1622 100644 --- a/k8s-operator/api-proxy/proxy.go +++ b/k8s-operator/api-proxy/proxy.go @@ -47,8 +47,8 @@ var ( // caller's Tailscale identity and the rules defined in the tailnet ACLs. // - false: the proxy is started and requests are passed through to the // Kubernetes API without any auth modifications. -func NewAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, ts *tsnet.Server, authMode bool, https bool) (*APIServerProxy, error) { - if !authMode { +func NewAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, ts *tsnet.Server, mode kubetypes.APIServerProxyMode, https bool) (*APIServerProxy, error) { + if mode == kubetypes.APIServerProxyModeNoAuth { restConfig = rest.AnonymousClientConfig(restConfig) } @@ -85,7 +85,7 @@ func NewAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, ts *tsn ap := &APIServerProxy{ log: zlog, lc: lc, - authMode: authMode, + authMode: mode == kubetypes.APIServerProxyModeAuth, https: https, upstreamURL: u, ts: ts, @@ -278,7 +278,7 @@ func (ap *APIServerProxy) sessionForProto(w http.ResponseWriter, r *http.Request Namespace: r.PathValue(namespaceNameKey), Log: ap.log, } - h := ksr.New(opts) + h := ksr.NewHijacker(opts) ap.rp.ServeHTTP(h, r.WithContext(whoIsKey.WithValue(r.Context(), who))) } diff --git a/k8s-operator/sessionrecording/hijacker.go b/k8s-operator/sessionrecording/hijacker.go index e8c534afc..675a9b1dd 100644 --- a/k8s-operator/sessionrecording/hijacker.go +++ b/k8s-operator/sessionrecording/hijacker.go @@ -57,7 +57,7 @@ var ( counterSessionRecordingsUploaded = clientmetric.NewCounter("k8s_auth_proxy_session_recordings_uploaded") ) -func New(opts HijackerOpts) *Hijacker { +func NewHijacker(opts HijackerOpts) *Hijacker { return &Hijacker{ ts: opts.TS, req: opts.Req, diff --git a/kube/k8s-proxy/conf/conf.go b/kube/k8s-proxy/conf/conf.go index a32e0c03e..fdb6301ac 100644 --- a/kube/k8s-proxy/conf/conf.go +++ b/kube/k8s-proxy/conf/conf.go @@ -14,6 +14,7 @@ import ( "net/netip" "github.com/tailscale/hujson" + "tailscale.com/kube/kubetypes" "tailscale.com/tailcfg" "tailscale.com/types/opt" ) @@ -66,10 +67,10 @@ type ConfigV1Alpha1 struct { } type APIServerProxyConfig struct { - Enabled opt.Bool `json:",omitempty"` // Whether to enable the API Server proxy. - AuthMode opt.Bool `json:",omitempty"` // Run in auth or noauth mode. - ServiceName *tailcfg.ServiceName `json:",omitempty"` // Name of the Tailscale Service to advertise. - IssueCerts opt.Bool `json:",omitempty"` // Whether this replica should issue TLS certs for the Tailscale Service. + Enabled opt.Bool `json:",omitempty"` // Whether to enable the API Server proxy. + Mode *kubetypes.APIServerProxyMode `json:",omitempty"` // "auth" or "noauth" mode. + ServiceName *tailcfg.ServiceName `json:",omitempty"` // Name of the Tailscale Service to advertise. + IssueCerts opt.Bool `json:",omitempty"` // Whether this replica should issue TLS certs for the Tailscale Service. } // Load reads and parses the config file at the provided path on disk. diff --git a/kube/kubetypes/types.go b/kube/kubetypes/types.go index 5e7d4cd1f..44b01fe1a 100644 --- a/kube/kubetypes/types.go +++ b/kube/kubetypes/types.go @@ -3,6 +3,8 @@ package kubetypes +import "fmt" + const ( // Hostinfo App values for the Tailscale Kubernetes Operator components. AppOperator = "k8s-operator" @@ -59,5 +61,24 @@ const ( LabelSecretTypeState = "state" LabelSecretTypeCerts = "certs" - KubeAPIServerConfigFile = "config.hujson" + KubeAPIServerConfigFile = "config.hujson" + APIServerProxyModeAuth APIServerProxyMode = "auth" + APIServerProxyModeNoAuth APIServerProxyMode = "noauth" ) + +// APIServerProxyMode specifies whether the API server proxy will add +// impersonation headers to requests based on the caller's Tailscale identity. +// May be "auth" or "noauth". +type APIServerProxyMode string + +func (a *APIServerProxyMode) UnmarshalJSON(data []byte) error { + switch string(data) { + case `"auth"`: + *a = APIServerProxyModeAuth + case `"noauth"`: + *a = APIServerProxyModeNoAuth + default: + return fmt.Errorf("unknown APIServerProxyMode %q", data) + } + return nil +} diff --git a/kube/kubetypes/types_test.go b/kube/kubetypes/types_test.go new file mode 100644 index 000000000..ea1846b32 --- /dev/null +++ b/kube/kubetypes/types_test.go @@ -0,0 +1,42 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package kubetypes + +import ( + "encoding/json" + "testing" +) + +func TestUnmarshalAPIServerProxyMode(t *testing.T) { + tests := []struct { + data string + expected APIServerProxyMode + }{ + {data: `{"mode":"auth"}`, expected: APIServerProxyModeAuth}, + {data: `{"mode":"noauth"}`, expected: APIServerProxyModeNoAuth}, + {data: `{"mode":""}`, expected: ""}, + {data: `{"mode":"Auth"}`, expected: ""}, + {data: `{"mode":"unknown"}`, expected: ""}, + } + + for _, tc := range tests { + var s struct { + Mode *APIServerProxyMode `json:",omitempty"` + } + err := json.Unmarshal([]byte(tc.data), &s) + if tc.expected == "" { + if err == nil { + t.Errorf("expected error for %q, got none", tc.data) + } + continue + } + if err != nil { + t.Errorf("unexpected error for %q: %v", tc.data, err) + continue + } + if *s.Mode != tc.expected { + t.Errorf("for %q expected %q, got %q", tc.data, tc.expected, *s.Mode) + } + } +}