Просмотр исходного кода

cmd/k8s-operator: add support for running an auth proxy

Updates #5055

Signed-off-by: Maisem Ali <[email protected]>
Maisem Ali 3 лет назад
Родитель
Сommit
05adf22383

+ 24 - 0
cmd/k8s-operator/manifests/authproxy-rbac.yaml

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

+ 2 - 0
cmd/k8s-operator/manifests/operator.yaml

@@ -148,6 +148,8 @@ spec:
               value: tailscale/tailscale:unstable
             - name: PROXY_TAGS
               value: tag:k8s
+            - name: AUTH_PROXY
+              value: "false"
           volumeMounts:
           - name: oauth
             mountPath: /oauth

+ 24 - 10
cmd/k8s-operator/operator.go

@@ -25,6 +25,7 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/fields"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/cache"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -51,15 +52,16 @@ func main() {
 	tailscale.I_Acknowledge_This_API_Is_Unstable = true
 
 	var (
-		hostname         = defaultEnv("OPERATOR_HOSTNAME", "tailscale-operator")
-		kubeSecret       = defaultEnv("OPERATOR_SECRET", "")
-		operatorTags     = defaultEnv("OPERATOR_INITIAL_TAGS", "tag:k8s-operator")
-		tsNamespace      = defaultEnv("OPERATOR_NAMESPACE", "")
-		tslogging        = defaultEnv("OPERATOR_LOGGING", "info")
-		clientIDPath     = defaultEnv("CLIENT_ID_FILE", "")
-		clientSecretPath = defaultEnv("CLIENT_SECRET_FILE", "")
-		image            = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
-		tags             = defaultEnv("PROXY_TAGS", "tag:k8s")
+		hostname           = defaultEnv("OPERATOR_HOSTNAME", "tailscale-operator")
+		kubeSecret         = defaultEnv("OPERATOR_SECRET", "")
+		operatorTags       = defaultEnv("OPERATOR_INITIAL_TAGS", "tag:k8s-operator")
+		tsNamespace        = defaultEnv("OPERATOR_NAMESPACE", "")
+		tslogging          = defaultEnv("OPERATOR_LOGGING", "info")
+		clientIDPath       = defaultEnv("CLIENT_ID_FILE", "")
+		clientSecretPath   = defaultEnv("CLIENT_SECRET_FILE", "")
+		image              = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
+		tags               = defaultEnv("PROXY_TAGS", "tag:k8s")
+		shouldRunAuthProxy = defaultEnv("AUTH_PROXY", "false")
 	)
 
 	var opts []kzap.Opts
@@ -173,7 +175,8 @@ waitOnline:
 	nsFilter := cache.ObjectSelector{
 		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{
 			SelectorsByObject: map[client.Object]cache.ObjectSelector{
 				&corev1.Secret{}:      nsFilter,
@@ -222,6 +225,17 @@ waitOnline:
 	}
 
 	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 {
 		startlog.Fatalf("could not start manager: %v", err)
 	}

+ 80 - 0
cmd/k8s-operator/proxy.go

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

+ 1 - 1
go.mod

@@ -86,6 +86,7 @@ require (
 	inet.af/wf v0.0.0-20220728202103-50d96caab2f6
 	k8s.io/api v0.25.0
 	k8s.io/apimachinery v0.25.0
+	k8s.io/client-go v0.25.0
 	nhooyr.io/websocket v1.8.7
 	sigs.k8s.io/controller-runtime v0.13.1
 	sigs.k8s.io/yaml v1.3.0
@@ -314,7 +315,6 @@ require (
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	howett.net/plist v1.0.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/klog/v2 v2.70.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect