mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-11-04 00:55:11 +00:00 
			
		
		
		
	cmd/k8s-operator: add support for running an auth proxy
Updates #5055 Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
		
							
								
								
									
										24
									
								
								cmd/k8s-operator/manifests/authproxy-rbac.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								cmd/k8s-operator/manifests/authproxy-rbac.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					# Copyright (c) Tailscale Inc & AUTHORS
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: BSD-3-Clause
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					apiVersion: rbac.authorization.k8s.io/v1
 | 
				
			||||||
 | 
					kind: ClusterRole
 | 
				
			||||||
 | 
					metadata:
 | 
				
			||||||
 | 
					  name: tailscale-auth-proxy
 | 
				
			||||||
 | 
					rules:
 | 
				
			||||||
 | 
					- apiGroups: [""]
 | 
				
			||||||
 | 
					  resources: ["users"]
 | 
				
			||||||
 | 
					  verbs: ["impersonate"]
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					apiVersion: rbac.authorization.k8s.io/v1
 | 
				
			||||||
 | 
					kind: ClusterRoleBinding
 | 
				
			||||||
 | 
					metadata:
 | 
				
			||||||
 | 
					  name: tailscale-auth-proxy
 | 
				
			||||||
 | 
					subjects:
 | 
				
			||||||
 | 
					- kind: ServiceAccount
 | 
				
			||||||
 | 
					  name: operator
 | 
				
			||||||
 | 
					  namespace: tailscale
 | 
				
			||||||
 | 
					roleRef:
 | 
				
			||||||
 | 
					  kind: ClusterRole
 | 
				
			||||||
 | 
					  name: tailscale-auth-proxy
 | 
				
			||||||
 | 
					  apiGroup: rbac.authorization.k8s.io
 | 
				
			||||||
@@ -148,6 +148,8 @@ spec:
 | 
				
			|||||||
              value: tailscale/tailscale:unstable
 | 
					              value: tailscale/tailscale:unstable
 | 
				
			||||||
            - name: PROXY_TAGS
 | 
					            - name: PROXY_TAGS
 | 
				
			||||||
              value: tag:k8s
 | 
					              value: tag:k8s
 | 
				
			||||||
 | 
					            - name: AUTH_PROXY
 | 
				
			||||||
 | 
					              value: "false"
 | 
				
			||||||
          volumeMounts:
 | 
					          volumeMounts:
 | 
				
			||||||
          - name: oauth
 | 
					          - name: oauth
 | 
				
			||||||
            mountPath: /oauth
 | 
					            mountPath: /oauth
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,6 +25,7 @@ import (
 | 
				
			|||||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
						"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/fields"
 | 
						"k8s.io/apimachinery/pkg/fields"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/types"
 | 
						"k8s.io/apimachinery/pkg/types"
 | 
				
			||||||
 | 
						"k8s.io/client-go/rest"
 | 
				
			||||||
	"sigs.k8s.io/controller-runtime/pkg/builder"
 | 
						"sigs.k8s.io/controller-runtime/pkg/builder"
 | 
				
			||||||
	"sigs.k8s.io/controller-runtime/pkg/cache"
 | 
						"sigs.k8s.io/controller-runtime/pkg/cache"
 | 
				
			||||||
	"sigs.k8s.io/controller-runtime/pkg/client"
 | 
						"sigs.k8s.io/controller-runtime/pkg/client"
 | 
				
			||||||
@@ -51,15 +52,16 @@ func main() {
 | 
				
			|||||||
	tailscale.I_Acknowledge_This_API_Is_Unstable = true
 | 
						tailscale.I_Acknowledge_This_API_Is_Unstable = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var (
 | 
						var (
 | 
				
			||||||
		hostname         = defaultEnv("OPERATOR_HOSTNAME", "tailscale-operator")
 | 
							hostname           = defaultEnv("OPERATOR_HOSTNAME", "tailscale-operator")
 | 
				
			||||||
		kubeSecret       = defaultEnv("OPERATOR_SECRET", "")
 | 
							kubeSecret         = defaultEnv("OPERATOR_SECRET", "")
 | 
				
			||||||
		operatorTags     = defaultEnv("OPERATOR_INITIAL_TAGS", "tag:k8s-operator")
 | 
							operatorTags       = defaultEnv("OPERATOR_INITIAL_TAGS", "tag:k8s-operator")
 | 
				
			||||||
		tsNamespace      = defaultEnv("OPERATOR_NAMESPACE", "")
 | 
							tsNamespace        = defaultEnv("OPERATOR_NAMESPACE", "")
 | 
				
			||||||
		tslogging        = defaultEnv("OPERATOR_LOGGING", "info")
 | 
							tslogging          = defaultEnv("OPERATOR_LOGGING", "info")
 | 
				
			||||||
		clientIDPath     = defaultEnv("CLIENT_ID_FILE", "")
 | 
							clientIDPath       = defaultEnv("CLIENT_ID_FILE", "")
 | 
				
			||||||
		clientSecretPath = defaultEnv("CLIENT_SECRET_FILE", "")
 | 
							clientSecretPath   = defaultEnv("CLIENT_SECRET_FILE", "")
 | 
				
			||||||
		image            = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
 | 
							image              = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
 | 
				
			||||||
		tags             = defaultEnv("PROXY_TAGS", "tag:k8s")
 | 
							tags               = defaultEnv("PROXY_TAGS", "tag:k8s")
 | 
				
			||||||
 | 
							shouldRunAuthProxy = defaultEnv("AUTH_PROXY", "false")
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var opts []kzap.Opts
 | 
						var opts []kzap.Opts
 | 
				
			||||||
@@ -173,7 +175,8 @@ waitOnline:
 | 
				
			|||||||
	nsFilter := cache.ObjectSelector{
 | 
						nsFilter := cache.ObjectSelector{
 | 
				
			||||||
		Field: fields.SelectorFromSet(fields.Set{"metadata.namespace": tsNamespace}),
 | 
							Field: fields.SelectorFromSet(fields.Set{"metadata.namespace": tsNamespace}),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{
 | 
						restConfig := config.GetConfigOrDie()
 | 
				
			||||||
 | 
						mgr, err := manager.New(restConfig, manager.Options{
 | 
				
			||||||
		NewCache: cache.BuilderWithOptions(cache.Options{
 | 
							NewCache: cache.BuilderWithOptions(cache.Options{
 | 
				
			||||||
			SelectorsByObject: map[client.Object]cache.ObjectSelector{
 | 
								SelectorsByObject: map[client.Object]cache.ObjectSelector{
 | 
				
			||||||
				&corev1.Secret{}:      nsFilter,
 | 
									&corev1.Secret{}:      nsFilter,
 | 
				
			||||||
@@ -222,6 +225,17 @@ waitOnline:
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	startlog.Infof("Startup complete, operator running")
 | 
						startlog.Infof("Startup complete, operator running")
 | 
				
			||||||
 | 
						if shouldRunAuthProxy == "true" {
 | 
				
			||||||
 | 
							rc, err := rest.TransportFor(restConfig)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								startlog.Fatalf("could not get rest transport: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							authProxyListener, err := s.Listen("tcp", ":443")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								startlog.Fatalf("could not listen on :443: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							go runAuthProxy(lc, authProxyListener, rc, zlog.Named("auth-proxy").Infof)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
 | 
						if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
 | 
				
			||||||
		startlog.Fatalf("could not start manager: %v", err)
 | 
							startlog.Fatalf("could not start manager: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										80
									
								
								cmd/k8s-operator/proxy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								cmd/k8s-operator/proxy.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					// Copyright (c) Tailscale Inc & AUTHORS
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: BSD-3-Clause
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"crypto/tls"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/http/httputil"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"tailscale.com/client/tailscale"
 | 
				
			||||||
 | 
						"tailscale.com/client/tailscale/apitype"
 | 
				
			||||||
 | 
						"tailscale.com/types/logger"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type whoIsKey struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// authProxy is an http.Handler that authenticates requests using the Tailscale
 | 
				
			||||||
 | 
					// LocalAPI and then proxies them to the Kubernetes API.
 | 
				
			||||||
 | 
					type authProxy struct {
 | 
				
			||||||
 | 
						logf logger.Logf
 | 
				
			||||||
 | 
						lc   *tailscale.LocalClient
 | 
				
			||||||
 | 
						rp   *httputil.ReverseProxy
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *authProxy) 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)
 | 
				
			||||||
 | 
							http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						r = r.WithContext(context.WithValue(r.Context(), whoIsKey{}, who))
 | 
				
			||||||
 | 
						h.rp.ServeHTTP(w, r)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func runAuthProxy(lc *tailscale.LocalClient, ls net.Listener, rt http.RoundTripper, logf logger.Logf) {
 | 
				
			||||||
 | 
						u, err := url.Parse(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("runAuthProxy: failed to parse URL %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ap := &authProxy{
 | 
				
			||||||
 | 
							logf: logf,
 | 
				
			||||||
 | 
							lc:   lc,
 | 
				
			||||||
 | 
							rp: &httputil.ReverseProxy{
 | 
				
			||||||
 | 
								Director: func(r *http.Request) {
 | 
				
			||||||
 | 
									// Replace the request with the user's identity.
 | 
				
			||||||
 | 
									who := r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
 | 
				
			||||||
 | 
									r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Remove all authentication headers.
 | 
				
			||||||
 | 
									r.Header.Del("Authorization")
 | 
				
			||||||
 | 
									r.Header.Del("Impersonate-Group")
 | 
				
			||||||
 | 
									r.Header.Del("Impersonate-Uid")
 | 
				
			||||||
 | 
									for k := range r.Header {
 | 
				
			||||||
 | 
										if strings.HasPrefix(k, "Impersonate-Extra-") {
 | 
				
			||||||
 | 
											r.Header.Del(k)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Replace the URL with the Kubernetes APIServer.
 | 
				
			||||||
 | 
									r.URL.Scheme = u.Scheme
 | 
				
			||||||
 | 
									r.URL.Host = u.Host
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								Transport: rt,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := http.Serve(tls.NewListener(ls, &tls.Config{
 | 
				
			||||||
 | 
							GetCertificate: lc.GetCertificate,
 | 
				
			||||||
 | 
						}), ap); err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("runAuthProxy: failed to serve %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@@ -86,6 +86,7 @@ require (
 | 
				
			|||||||
	inet.af/wf v0.0.0-20220728202103-50d96caab2f6
 | 
						inet.af/wf v0.0.0-20220728202103-50d96caab2f6
 | 
				
			||||||
	k8s.io/api v0.25.0
 | 
						k8s.io/api v0.25.0
 | 
				
			||||||
	k8s.io/apimachinery v0.25.0
 | 
						k8s.io/apimachinery v0.25.0
 | 
				
			||||||
 | 
						k8s.io/client-go v0.25.0
 | 
				
			||||||
	nhooyr.io/websocket v1.8.7
 | 
						nhooyr.io/websocket v1.8.7
 | 
				
			||||||
	sigs.k8s.io/controller-runtime v0.13.1
 | 
						sigs.k8s.io/controller-runtime v0.13.1
 | 
				
			||||||
	sigs.k8s.io/yaml v1.3.0
 | 
						sigs.k8s.io/yaml v1.3.0
 | 
				
			||||||
@@ -314,7 +315,6 @@ require (
 | 
				
			|||||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
						gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
				
			||||||
	howett.net/plist v1.0.0 // indirect
 | 
						howett.net/plist v1.0.0 // indirect
 | 
				
			||||||
	k8s.io/apiextensions-apiserver v0.25.0 // indirect
 | 
						k8s.io/apiextensions-apiserver v0.25.0 // indirect
 | 
				
			||||||
	k8s.io/client-go v0.25.0 // indirect
 | 
					 | 
				
			||||||
	k8s.io/component-base v0.25.0 // indirect
 | 
						k8s.io/component-base v0.25.0 // indirect
 | 
				
			||||||
	k8s.io/klog/v2 v2.70.1 // indirect
 | 
						k8s.io/klog/v2 v2.70.1 // indirect
 | 
				
			||||||
	k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
 | 
						k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user