k8s-operator/api-proxy: resolve capmaps for services

This commit modifies the k8s api server proxy to use the local
`WhoIsForService` method when actively called behind a tailscale
service.

The proxy predates services and was originally written to act as a
single instance running in-process alongside the operator. Since then
services have enabled us to provide a HA api server proxy. Recently,
app caps were applied to tailscale services meaning our api server
proxy also needs to take those into account to avoid confused
deputy type scenarios.

Closes: https://github.com/tailscale/corp/issues/42001
Signed-off-by: David Bond <davidsbond93@gmail.com>
This commit is contained in:
David Bond 2026-05-20 11:51:03 +01:00
parent 95d874e9b4
commit 166d59f739
No known key found for this signature in database
GPG Key ID: A35B34F344ED7AFE
3 changed files with 18 additions and 6 deletions

View File

@ -134,7 +134,7 @@ func main() {
defer s.Close()
restConfig := config.GetConfigOrDie()
if mode != nil {
ap, err := apiproxy.NewAPIServerProxy(zlog, restConfig, s, *mode, true)
ap, err := apiproxy.NewAPIServerProxy(zlog, restConfig, s, *mode, true, "")
if err != nil {
zlog.Fatalf("error creating API server proxy: %v", err)
}

View File

@ -29,6 +29,7 @@
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/utils/strings/slices"
"tailscale.com/client/local"
"tailscale.com/cmd/k8s-proxy/internal/config"
"tailscale.com/health"
@ -374,7 +375,7 @@ func run(logger *zap.SugaredLogger) error {
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, mode, false)
ap, err := apiproxy.NewAPIServerProxy(logger.Named("apiserver-proxy"), restConfig, ts, mode, false, apiServerProxyService(cfg))
if err != nil {
return fmt.Errorf("error creating api server proxy: %w", err)
}
@ -482,7 +483,7 @@ func apiServerProxyService(cfg *conf.Config) tailcfg.ServiceName {
cfg.Parsed.APIServerProxy.Enabled.EqualBool(true) &&
cfg.Parsed.APIServerProxy.ServiceName != nil &&
*cfg.Parsed.APIServerProxy.ServiceName != "" {
return tailcfg.ServiceName(*cfg.Parsed.APIServerProxy.ServiceName)
return *cfg.Parsed.APIServerProxy.ServiceName
}
return ""

View File

@ -27,6 +27,7 @@
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/client-go/rest"
"k8s.io/client-go/transport"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
ksr "tailscale.com/k8s-operator/sessionrecording"
@ -54,7 +55,7 @@
// 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, mode kubetypes.APIServerProxyMode, https bool) (*APIServerProxy, error) {
func NewAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, ts *tsnet.Server, mode kubetypes.APIServerProxyMode, https bool, svcName tailcfg.ServiceName) (*APIServerProxy, error) {
if mode == kubetypes.APIServerProxyModeNoAuth {
restConfig = rest.AnonymousClientConfig(restConfig)
}
@ -94,6 +95,7 @@ func NewAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, ts *tsn
lc: lc,
authMode: mode == kubetypes.APIServerProxyModeAuth,
https: https,
svcName: svcName,
upstreamURL: u,
ts: ts,
sendEventFunc: sessionrecording.SendEvent,
@ -196,6 +198,7 @@ type APIServerProxy struct {
authMode bool // Whether to run with impersonation using caller's tailnet identity.
https bool // Whether to serve on https for the device hostname; true for k8s-operator, false (and localhost) for k8s-proxy.
svcName tailcfg.ServiceName
ts *tsnet.Server
hs *http.Server
upstreamURL *url.URL
@ -470,7 +473,7 @@ func (ap *APIServerProxy) addImpersonationHeadersAsRequired(r *http.Request) {
}
func (ap *APIServerProxy) whoIs(r *http.Request) (*apitype.WhoIsResponse, error) {
who, remoteErr := ap.lc.WhoIs(r.Context(), r.RemoteAddr)
who, remoteErr := ap.whoIsAddr(r.Context(), r.RemoteAddr)
if remoteErr == nil {
ap.log.Debugf("WhoIs from remote addr: %s", r.RemoteAddr)
return who, nil
@ -479,7 +482,7 @@ func (ap *APIServerProxy) whoIs(r *http.Request) (*apitype.WhoIsResponse, error)
var fwdErr error
fwdFor := r.Header.Get("X-Forwarded-For")
if fwdFor != "" && !ap.https {
who, fwdErr = ap.lc.WhoIs(r.Context(), fwdFor)
who, fwdErr = ap.whoIsAddr(r.Context(), fwdFor)
if fwdErr == nil {
ap.log.Debugf("WhoIs from X-Forwarded-For header: %s", fwdFor)
return who, nil
@ -489,6 +492,14 @@ func (ap *APIServerProxy) whoIs(r *http.Request) (*apitype.WhoIsResponse, error)
return nil, errors.Join(remoteErr, fwdErr)
}
func (ap *APIServerProxy) whoIsAddr(ctx context.Context, addr string) (*apitype.WhoIsResponse, error) {
if ap.svcName != "" {
return ap.lc.WhoIsForService(ctx, addr, ap.svcName)
}
return ap.lc.WhoIs(ctx, addr)
}
func (ap *APIServerProxy) authError(w http.ResponseWriter, err error) {
ap.log.Errorf("failed to authenticate caller: %v", err)
http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)