From 05adf223830d142c5d37ccfb9ca5f6d96a71fc8e Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Fri, 3 Feb 2023 14:47:52 -0800 Subject: [PATCH] cmd/k8s-operator: add support for running an auth proxy Updates #5055 Signed-off-by: Maisem Ali --- .../manifests/authproxy-rbac.yaml | 24 ++++++ cmd/k8s-operator/manifests/operator.yaml | 2 + cmd/k8s-operator/operator.go | 34 +++++--- cmd/k8s-operator/proxy.go | 80 +++++++++++++++++++ go.mod | 2 +- 5 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 cmd/k8s-operator/manifests/authproxy-rbac.yaml create mode 100644 cmd/k8s-operator/proxy.go diff --git a/cmd/k8s-operator/manifests/authproxy-rbac.yaml b/cmd/k8s-operator/manifests/authproxy-rbac.yaml new file mode 100644 index 000000000..ae711a858 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/cmd/k8s-operator/manifests/operator.yaml b/cmd/k8s-operator/manifests/operator.yaml index 6d4eb0ac1..c128d2b09 100644 --- a/cmd/k8s-operator/manifests/operator.yaml +++ b/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 diff --git a/cmd/k8s-operator/operator.go b/cmd/k8s-operator/operator.go index 3ef70539c..5d9824de7 100644 --- a/cmd/k8s-operator/operator.go +++ b/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) } diff --git a/cmd/k8s-operator/proxy.go b/cmd/k8s-operator/proxy.go new file mode 100644 index 000000000..83a4cfb81 --- /dev/null +++ b/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) + } +} diff --git a/go.mod b/go.mod index a6f2e9517..2f0bd690c 100644 --- a/go.mod +++ b/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