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 <tomhjp@users.noreply.github.com>
This commit is contained in:
Tom Proctor 2025-07-22 14:46:38 +01:00 committed by GitHub
parent 6f7e78b10f
commit 22a8e0ac50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 99 additions and 48 deletions

View File

@ -9,30 +9,12 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"tailscale.com/kube/kubetypes"
"tailscale.com/types/ptr"
) )
type apiServerProxyMode int func parseAPIProxyMode() *kubetypes.APIServerProxyMode {
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 {
haveAuthProxyEnv := os.Getenv("AUTH_PROXY") != "" haveAuthProxyEnv := os.Getenv("AUTH_PROXY") != ""
haveAPIProxyEnv := os.Getenv("APISERVER_PROXY") != "" haveAPIProxyEnv := os.Getenv("APISERVER_PROXY") != ""
switch { switch {
@ -41,21 +23,21 @@ func parseAPIProxyMode() apiServerProxyMode {
case haveAuthProxyEnv: case haveAuthProxyEnv:
var authProxyEnv = defaultBool("AUTH_PROXY", false) // deprecated var authProxyEnv = defaultBool("AUTH_PROXY", false) // deprecated
if authProxyEnv { if authProxyEnv {
return apiServerProxyModeEnabled return ptr.To(kubetypes.APIServerProxyModeAuth)
} }
return apiServerProxyModeDisabled return nil
case haveAPIProxyEnv: case haveAPIProxyEnv:
var apiProxyEnv = defaultEnv("APISERVER_PROXY", "") // true, false or "noauth" var apiProxyEnv = defaultEnv("APISERVER_PROXY", "") // true, false or "noauth"
switch apiProxyEnv { switch apiProxyEnv {
case "true": case "true":
return apiServerProxyModeEnabled return ptr.To(kubetypes.APIServerProxyModeAuth)
case "false", "": case "false", "":
return apiServerProxyModeDisabled return nil
case "noauth": case "noauth":
return apiServerProxyModeNoAuth return ptr.To(kubetypes.APIServerProxyModeNoAuth)
default: default:
panic(fmt.Sprintf("unknown APISERVER_PROXY value %q", apiProxyEnv)) panic(fmt.Sprintf("unknown APISERVER_PROXY value %q", apiProxyEnv))
} }
} }
return apiServerProxyModeDisabled return nil
} }

View File

@ -113,7 +113,7 @@ func main() {
// additionally act as api-server proxy // 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. // https://tailscale.com/kb/1236/kubernetes-operator/?q=kubernetes#accessing-the-kubernetes-control-plane-using-an-api-server-proxy.
mode := parseAPIProxyMode() mode := parseAPIProxyMode()
if mode == apiServerProxyModeDisabled { if mode == nil {
hostinfo.SetApp(kubetypes.AppOperator) hostinfo.SetApp(kubetypes.AppOperator)
} else { } else {
hostinfo.SetApp(kubetypes.AppInProcessAPIServerProxy) hostinfo.SetApp(kubetypes.AppInProcessAPIServerProxy)
@ -122,8 +122,8 @@ func main() {
s, tsc := initTSNet(zlog, loginServer) s, tsc := initTSNet(zlog, loginServer)
defer s.Close() defer s.Close()
restConfig := config.GetConfigOrDie() restConfig := config.GetConfigOrDie()
if mode != apiServerProxyModeDisabled { if mode != nil {
ap, err := apiproxy.NewAPIServerProxy(zlog, restConfig, s, mode == apiServerProxyModeEnabled, true) ap, err := apiproxy.NewAPIServerProxy(zlog, restConfig, s, *mode, true)
if err != nil { if err != nil {
zlog.Fatalf("error creating API server proxy: %v", err) zlog.Fatalf("error creating API server proxy: %v", err)
} }

View File

@ -805,6 +805,10 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
} }
} }
mode := kubetypes.APIServerProxyModeAuth
if !isAuthAPIServerProxy(pg) {
mode = kubetypes.APIServerProxyModeNoAuth
}
cfg := conf.VersionedConfig{ cfg := conf.VersionedConfig{
Version: "v1alpha1", Version: "v1alpha1",
ConfigV1Alpha1: &conf.ConfigV1Alpha1{ ConfigV1Alpha1: &conf.ConfigV1Alpha1{
@ -816,8 +820,8 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
// Reloadable fields. // Reloadable fields.
Hostname: &hostname, Hostname: &hostname,
APIServerProxy: &conf.APIServerProxyConfig{ APIServerProxy: &conf.APIServerProxyConfig{
Enabled: opt.NewBool(true), Enabled: opt.NewBool(true),
AuthMode: opt.NewBool(isAuthAPIServerProxy(pg)), Mode: &mode,
// The first replica is elected as the cert issuer, same // The first replica is elected as the cert issuer, same
// as containerboot does for ingress-pg-reconciler. // as containerboot does for ingress-pg-reconciler.
IssueCerts: opt.NewBool(i == 0), IssueCerts: opt.NewBool(i == 0),

View File

@ -1376,7 +1376,7 @@ func TestKubeAPIServerType_DoesNotOverwriteServicesConfig(t *testing.T) {
Hostname: ptr.To("test-k8s-apiserver-0"), Hostname: ptr.To("test-k8s-apiserver-0"),
APIServerProxy: &conf.APIServerProxyConfig{ APIServerProxy: &conf.APIServerProxyConfig{
Enabled: opt.NewBool(true), Enabled: opt.NewBool(true),
AuthMode: opt.NewBool(false), Mode: ptr.To(kubetypes.APIServerProxyModeNoAuth),
IssueCerts: opt.NewBool(true), IssueCerts: opt.NewBool(true),
}, },
}, },

View File

@ -34,6 +34,7 @@ import (
apiproxy "tailscale.com/k8s-operator/api-proxy" apiproxy "tailscale.com/k8s-operator/api-proxy"
"tailscale.com/kube/certs" "tailscale.com/kube/certs"
"tailscale.com/kube/k8s-proxy/conf" "tailscale.com/kube/k8s-proxy/conf"
"tailscale.com/kube/kubetypes"
klc "tailscale.com/kube/localclient" klc "tailscale.com/kube/localclient"
"tailscale.com/kube/services" "tailscale.com/kube/services"
"tailscale.com/kube/state" "tailscale.com/kube/state"
@ -238,11 +239,11 @@ func run(logger *zap.SugaredLogger) error {
} }
// Setup for the API server proxy. // Setup for the API server proxy.
authMode := true mode := kubetypes.APIServerProxyModeAuth
if cfg.Parsed.APIServerProxy != nil && cfg.Parsed.APIServerProxy.AuthMode.EqualBool(false) { if cfg.Parsed.APIServerProxy != nil && cfg.Parsed.APIServerProxy.Mode != nil {
authMode = false 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 { if err != nil {
return fmt.Errorf("error creating api server proxy: %w", err) return fmt.Errorf("error creating api server proxy: %w", err)
} }

View File

@ -47,8 +47,8 @@ var (
// caller's Tailscale identity and the rules defined in the tailnet ACLs. // caller's Tailscale identity and the rules defined in the tailnet ACLs.
// - false: the proxy is started and requests are passed through to the // - false: the proxy is started and requests are passed through to the
// Kubernetes API without any auth modifications. // Kubernetes API without any auth modifications.
func NewAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, ts *tsnet.Server, authMode bool, https bool) (*APIServerProxy, error) { func NewAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, ts *tsnet.Server, mode kubetypes.APIServerProxyMode, https bool) (*APIServerProxy, error) {
if !authMode { if mode == kubetypes.APIServerProxyModeNoAuth {
restConfig = rest.AnonymousClientConfig(restConfig) restConfig = rest.AnonymousClientConfig(restConfig)
} }
@ -85,7 +85,7 @@ func NewAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, ts *tsn
ap := &APIServerProxy{ ap := &APIServerProxy{
log: zlog, log: zlog,
lc: lc, lc: lc,
authMode: authMode, authMode: mode == kubetypes.APIServerProxyModeAuth,
https: https, https: https,
upstreamURL: u, upstreamURL: u,
ts: ts, ts: ts,
@ -278,7 +278,7 @@ func (ap *APIServerProxy) sessionForProto(w http.ResponseWriter, r *http.Request
Namespace: r.PathValue(namespaceNameKey), Namespace: r.PathValue(namespaceNameKey),
Log: ap.log, Log: ap.log,
} }
h := ksr.New(opts) h := ksr.NewHijacker(opts)
ap.rp.ServeHTTP(h, r.WithContext(whoIsKey.WithValue(r.Context(), who))) ap.rp.ServeHTTP(h, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
} }

View File

@ -57,7 +57,7 @@ var (
counterSessionRecordingsUploaded = clientmetric.NewCounter("k8s_auth_proxy_session_recordings_uploaded") counterSessionRecordingsUploaded = clientmetric.NewCounter("k8s_auth_proxy_session_recordings_uploaded")
) )
func New(opts HijackerOpts) *Hijacker { func NewHijacker(opts HijackerOpts) *Hijacker {
return &Hijacker{ return &Hijacker{
ts: opts.TS, ts: opts.TS,
req: opts.Req, req: opts.Req,

View File

@ -14,6 +14,7 @@ import (
"net/netip" "net/netip"
"github.com/tailscale/hujson" "github.com/tailscale/hujson"
"tailscale.com/kube/kubetypes"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/opt" "tailscale.com/types/opt"
) )
@ -66,10 +67,10 @@ type ConfigV1Alpha1 struct {
} }
type APIServerProxyConfig struct { type APIServerProxyConfig struct {
Enabled opt.Bool `json:",omitempty"` // Whether to enable the API Server proxy. Enabled opt.Bool `json:",omitempty"` // Whether to enable the API Server proxy.
AuthMode opt.Bool `json:",omitempty"` // Run in auth or noauth mode. Mode *kubetypes.APIServerProxyMode `json:",omitempty"` // "auth" or "noauth" mode.
ServiceName *tailcfg.ServiceName `json:",omitempty"` // Name of the Tailscale Service to advertise. 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. 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. // Load reads and parses the config file at the provided path on disk.

View File

@ -3,6 +3,8 @@
package kubetypes package kubetypes
import "fmt"
const ( const (
// Hostinfo App values for the Tailscale Kubernetes Operator components. // Hostinfo App values for the Tailscale Kubernetes Operator components.
AppOperator = "k8s-operator" AppOperator = "k8s-operator"
@ -59,5 +61,24 @@ const (
LabelSecretTypeState = "state" LabelSecretTypeState = "state"
LabelSecretTypeCerts = "certs" 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
}

View File

@ -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)
}
}
}