mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-18 02:48:40 +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:
parent
31e2e9a300
commit
05adf22383
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user