mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-30 07:43:42 +00:00
cmd/{k8s-operator,k8s-proxy},k8s-operator,kube: add new k8s-proxy command
Refactors the proxy library interface to suit being a library better and adds a new k8s-proxy command, alongside Makefile and build_docker.sh updates to build a container out of it. Most features intentionally missing for now to act as a base/MVP version of the proxy command. Updates #13358 Change-Id: I21580db1875d2e64d72c4c988fe11c55f5cd6ae5 Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
parent
c4fb380f3c
commit
9b88169de7
39
Makefile
39
Makefile
@ -90,30 +90,33 @@ pushspk: spk ## Push and install synology package on ${SYNO_HOST} host
|
|||||||
scp tailscale.spk root@${SYNO_HOST}:
|
scp tailscale.spk root@${SYNO_HOST}:
|
||||||
ssh root@${SYNO_HOST} /usr/syno/bin/synopkg install tailscale.spk
|
ssh root@${SYNO_HOST} /usr/syno/bin/synopkg install tailscale.spk
|
||||||
|
|
||||||
publishdevimage: ## Build and publish tailscale image to location specified by ${REPO}
|
check-image-repo:
|
||||||
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
|
@if [ -z "$(REPO)" ]; then \
|
||||||
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
|
echo "REPO=... required; e.g. REPO=ghcr.io/$$USER/tailscale" >&2; \
|
||||||
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
|
exit 1; \
|
||||||
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
|
fi
|
||||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
|
@for repo in tailscale/tailscale ghcr.io/tailscale/tailscale \
|
||||||
|
tailscale/k8s-operator ghcr.io/tailscale/k8s-operator \
|
||||||
|
tailscale/k8s-nameserver ghcr.io/tailscale/k8s-nameserver \
|
||||||
|
tailscale/k8s-proxy ghcr.io/tailscale/k8s-proxy; do \
|
||||||
|
if [ "$(REPO)" = "$$repo" ]; then \
|
||||||
|
echo "REPO=... must not be $$repo" >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
publishdevimage: check-image-repo ## Build and publish tailscale image to location specified by ${REPO}
|
||||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=client ./build_docker.sh
|
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=client ./build_docker.sh
|
||||||
|
|
||||||
publishdevoperator: ## Build and publish k8s-operator image to location specified by ${REPO}
|
publishdevoperator: check-image-repo ## Build and publish k8s-operator image to location specified by ${REPO}
|
||||||
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
|
|
||||||
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
|
|
||||||
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
|
|
||||||
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
|
|
||||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
|
|
||||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=k8s-operator ./build_docker.sh
|
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=k8s-operator ./build_docker.sh
|
||||||
|
|
||||||
publishdevnameserver: ## Build and publish k8s-nameserver image to location specified by ${REPO}
|
publishdevnameserver: check-image-repo ## Build and publish k8s-nameserver image to location specified by ${REPO}
|
||||||
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
|
|
||||||
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
|
|
||||||
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
|
|
||||||
@test "${REPO}" != "tailscale/k8s-nameserver" || (echo "REPO=... must not be tailscale/k8s-nameserver" && exit 1)
|
|
||||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-nameserver" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-nameserver" && exit 1)
|
|
||||||
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=k8s-nameserver ./build_docker.sh
|
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=k8s-nameserver ./build_docker.sh
|
||||||
|
|
||||||
|
publishdevproxy: check-image-repo ## Build and publish k8s-proxy image to location specified by ${REPO}
|
||||||
|
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=k8s-proxy ./build_docker.sh
|
||||||
|
|
||||||
.PHONY: sshintegrationtest
|
.PHONY: sshintegrationtest
|
||||||
sshintegrationtest: ## Run the SSH integration tests in various Docker containers
|
sshintegrationtest: ## Run the SSH integration tests in various Docker containers
|
||||||
@GOOS=linux GOARCH=amd64 ./tool/go test -tags integrationtest -c ./ssh/tailssh -o ssh/tailssh/testcontainers/tailssh.test && \
|
@GOOS=linux GOARCH=amd64 ./tool/go test -tags integrationtest -c ./ssh/tailssh -o ssh/tailssh/testcontainers/tailssh.test && \
|
||||||
|
@ -90,6 +90,24 @@ case "$TARGET" in
|
|||||||
--annotations="${ANNOTATIONS}" \
|
--annotations="${ANNOTATIONS}" \
|
||||||
/usr/local/bin/k8s-nameserver
|
/usr/local/bin/k8s-nameserver
|
||||||
;;
|
;;
|
||||||
|
k8s-proxy)
|
||||||
|
DEFAULT_REPOS="tailscale/k8s-proxy"
|
||||||
|
REPOS="${REPOS:-${DEFAULT_REPOS}}"
|
||||||
|
go run github.com/tailscale/mkctr \
|
||||||
|
--gopaths="tailscale.com/cmd/k8s-proxy:/usr/local/bin/k8s-proxy" \
|
||||||
|
--ldflags=" \
|
||||||
|
-X tailscale.com/version.longStamp=${VERSION_LONG} \
|
||||||
|
-X tailscale.com/version.shortStamp=${VERSION_SHORT} \
|
||||||
|
-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" \
|
||||||
|
--base="${BASE}" \
|
||||||
|
--tags="${TAGS}" \
|
||||||
|
--gotags="ts_kube,ts_package_container" \
|
||||||
|
--repos="${REPOS}" \
|
||||||
|
--push="${PUSH}" \
|
||||||
|
--target="${PLATFORM}" \
|
||||||
|
--annotations="${ANNOTATIONS}" \
|
||||||
|
/usr/local/bin/k8s-proxy
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "unknown target: $TARGET"
|
echo "unknown target: $TARGET"
|
||||||
exit 1
|
exit 1
|
||||||
|
@ -103,8 +103,8 @@ func main() {
|
|||||||
// The operator can run either as a plain operator or it can
|
// The operator can run either as a plain operator or it can
|
||||||
// additionally act as api-server proxy
|
// additionally act as api-server proxy
|
||||||
// https://tailscale.com/kb/1236/kubernetes-operator/?q=kubernetes#accessing-the-kubernetes-control-plane-using-an-api-server-proxy.
|
// https://tailscale.com/kb/1236/kubernetes-operator/?q=kubernetes#accessing-the-kubernetes-control-plane-using-an-api-server-proxy.
|
||||||
mode := apiproxy.ParseAPIProxyMode()
|
mode := parseAPIProxyMode()
|
||||||
if mode == apiproxy.APIServerProxyModeDisabled {
|
if mode == apiServerProxyModeDisabled {
|
||||||
hostinfo.SetApp(kubetypes.AppOperator)
|
hostinfo.SetApp(kubetypes.AppOperator)
|
||||||
} else {
|
} else {
|
||||||
hostinfo.SetApp(kubetypes.AppAPIServerProxy)
|
hostinfo.SetApp(kubetypes.AppAPIServerProxy)
|
||||||
@ -113,7 +113,17 @@ func main() {
|
|||||||
s, tsc := initTSNet(zlog)
|
s, tsc := initTSNet(zlog)
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
restConfig := config.GetConfigOrDie()
|
restConfig := config.GetConfigOrDie()
|
||||||
apiproxy.MaybeLaunchAPIServerProxy(zlog, restConfig, s, mode)
|
if mode != apiServerProxyModeDisabled {
|
||||||
|
ap, err := apiproxy.NewAPIServerProxy(zlog, restConfig, s, mode == apiServerProxyModeEnabled)
|
||||||
|
if err != nil {
|
||||||
|
zlog.Fatalf("error creating API server proxy: %v", err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if err := ap.Run(); err != nil {
|
||||||
|
zlog.Fatalf("error running API server proxy: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
rOpts := reconcilerOpts{
|
rOpts := reconcilerOpts{
|
||||||
log: zlog,
|
log: zlog,
|
||||||
tsServer: s,
|
tsServer: s,
|
||||||
|
61
cmd/k8s-operator/proxy.go
Normal file
61
cmd/k8s-operator/proxy.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !plan9
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiServerProxyMode int
|
||||||
|
|
||||||
|
func (a apiServerProxyMode) String() string {
|
||||||
|
switch a {
|
||||||
|
case apiServerProxyModeDisabled:
|
||||||
|
return "disabled"
|
||||||
|
case apiServerProxyModeEnabled:
|
||||||
|
return "auth"
|
||||||
|
case apiServerProxyModeNoAuth:
|
||||||
|
return "noauth"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
apiServerProxyModeDisabled apiServerProxyMode = iota
|
||||||
|
apiServerProxyModeEnabled
|
||||||
|
apiServerProxyModeNoAuth
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseAPIProxyMode() apiServerProxyMode {
|
||||||
|
haveAuthProxyEnv := os.Getenv("AUTH_PROXY") != ""
|
||||||
|
haveAPIProxyEnv := os.Getenv("APISERVER_PROXY") != ""
|
||||||
|
switch {
|
||||||
|
case haveAPIProxyEnv && haveAuthProxyEnv:
|
||||||
|
log.Fatal("AUTH_PROXY and APISERVER_PROXY are mutually exclusive")
|
||||||
|
case haveAuthProxyEnv:
|
||||||
|
var authProxyEnv = defaultBool("AUTH_PROXY", false) // deprecated
|
||||||
|
if authProxyEnv {
|
||||||
|
return apiServerProxyModeEnabled
|
||||||
|
}
|
||||||
|
return apiServerProxyModeDisabled
|
||||||
|
case haveAPIProxyEnv:
|
||||||
|
var apiProxyEnv = defaultEnv("APISERVER_PROXY", "") // true, false or "noauth"
|
||||||
|
switch apiProxyEnv {
|
||||||
|
case "true":
|
||||||
|
return apiServerProxyModeEnabled
|
||||||
|
case "false", "":
|
||||||
|
return apiServerProxyModeDisabled
|
||||||
|
case "noauth":
|
||||||
|
return apiServerProxyModeNoAuth
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown APISERVER_PROXY value %q", apiProxyEnv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apiServerProxyModeDisabled
|
||||||
|
}
|
93
cmd/k8s-proxy/main.go
Normal file
93
cmd/k8s-proxy/main.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !plan9
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/go-logr/zapr"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
kzap "sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||||
|
"tailscale.com/hostinfo"
|
||||||
|
"tailscale.com/ipn/store/kubestore"
|
||||||
|
apiproxy "tailscale.com/k8s-operator/api-proxy"
|
||||||
|
"tailscale.com/kube/kubetypes"
|
||||||
|
"tailscale.com/tsnet"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
var (
|
||||||
|
podName = os.Getenv("POD_NAME")
|
||||||
|
)
|
||||||
|
|
||||||
|
if podName == "" {
|
||||||
|
return fmt.Errorf("POD_NAME environment variable is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts []kzap.Opts
|
||||||
|
switch "dev" { // TODO(tomhjp): make configurable
|
||||||
|
case "info":
|
||||||
|
opts = append(opts, kzap.Level(zapcore.InfoLevel))
|
||||||
|
case "debug":
|
||||||
|
opts = append(opts, kzap.Level(zapcore.DebugLevel))
|
||||||
|
case "dev":
|
||||||
|
opts = append(opts, kzap.UseDevMode(true), kzap.Level(zapcore.DebugLevel))
|
||||||
|
}
|
||||||
|
zlog := kzap.NewRaw(opts...).Sugar()
|
||||||
|
logf.SetLogger(zapr.NewLogger(zlog.Desugar()))
|
||||||
|
hostinfo.SetApp(kubetypes.AppProxy) // TODO(tomhjp): Advertise auth/noauth as well?
|
||||||
|
|
||||||
|
authMode := true // TODO(tomhjp): make configurable
|
||||||
|
st, err := kubestore.New(logger.Discard, podName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating kubestore: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := &tsnet.Server{
|
||||||
|
Hostname: podName, // TODO(tomhjp): make configurable
|
||||||
|
Logf: zlog.Named("tailscaled").Debugf,
|
||||||
|
Store: st,
|
||||||
|
}
|
||||||
|
if _, err := ts.Up(context.Background()); err != nil {
|
||||||
|
return fmt.Errorf("error starting tailscale server: %v", err)
|
||||||
|
}
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
restConfig, err := config.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting kubeconfig: %w", err)
|
||||||
|
}
|
||||||
|
ap, err := apiproxy.NewAPIServerProxy(zlog, restConfig, ts, authMode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating api server proxy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-sig
|
||||||
|
ap.Close()
|
||||||
|
}()
|
||||||
|
if err := ap.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -6,17 +6,17 @@
|
|||||||
package apiproxy
|
package apiproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/transport"
|
"k8s.io/client-go/transport"
|
||||||
@ -37,123 +37,49 @@ var (
|
|||||||
whoIsKey = ctxkey.New("", (*apitype.WhoIsResponse)(nil))
|
whoIsKey = ctxkey.New("", (*apitype.WhoIsResponse)(nil))
|
||||||
)
|
)
|
||||||
|
|
||||||
type APIServerProxyMode int
|
// NewAPIServerProxy creates a new APIServerProxy that's ready to start once Run
|
||||||
|
// is called. No network traffic will flow until Run is called.
|
||||||
func (a APIServerProxyMode) String() string {
|
//
|
||||||
switch a {
|
// authMode controls how the proxy behaves:
|
||||||
case APIServerProxyModeDisabled:
|
// - true: the proxy is started and requests are impersonated using the
|
||||||
return "disabled"
|
// caller's Tailscale identity and the rules defined in the tailnet ACLs.
|
||||||
case APIServerProxyModeEnabled:
|
// - false: the proxy is started and requests are passed through to the
|
||||||
return "auth"
|
// Kubernetes API without any auth modifications.
|
||||||
case APIServerProxyModeNoAuth:
|
func NewAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, ts *tsnet.Server, authMode bool) (*APIServerProxy, error) {
|
||||||
return "noauth"
|
if !authMode {
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
APIServerProxyModeDisabled APIServerProxyMode = iota
|
|
||||||
APIServerProxyModeEnabled
|
|
||||||
APIServerProxyModeNoAuth
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseAPIProxyMode() APIServerProxyMode {
|
|
||||||
haveAuthProxyEnv := os.Getenv("AUTH_PROXY") != ""
|
|
||||||
haveAPIProxyEnv := os.Getenv("APISERVER_PROXY") != ""
|
|
||||||
switch {
|
|
||||||
case haveAPIProxyEnv && haveAuthProxyEnv:
|
|
||||||
log.Fatal("AUTH_PROXY and APISERVER_PROXY are mutually exclusive")
|
|
||||||
case haveAuthProxyEnv:
|
|
||||||
var authProxyEnv = defaultBool("AUTH_PROXY", false) // deprecated
|
|
||||||
if authProxyEnv {
|
|
||||||
return APIServerProxyModeEnabled
|
|
||||||
}
|
|
||||||
return APIServerProxyModeDisabled
|
|
||||||
case haveAPIProxyEnv:
|
|
||||||
var apiProxyEnv = defaultEnv("APISERVER_PROXY", "") // true, false or "noauth"
|
|
||||||
switch apiProxyEnv {
|
|
||||||
case "true":
|
|
||||||
return APIServerProxyModeEnabled
|
|
||||||
case "false", "":
|
|
||||||
return APIServerProxyModeDisabled
|
|
||||||
case "noauth":
|
|
||||||
return APIServerProxyModeNoAuth
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown APISERVER_PROXY value %q", apiProxyEnv))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return APIServerProxyModeDisabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// maybeLaunchAPIServerProxy launches the auth proxy, which is a small HTTP server
|
|
||||||
// that authenticates requests using the Tailscale LocalAPI and then proxies
|
|
||||||
// them to the kube-apiserver.
|
|
||||||
func MaybeLaunchAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, s *tsnet.Server, mode APIServerProxyMode) {
|
|
||||||
if mode == APIServerProxyModeDisabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
startlog := zlog.Named("launchAPIProxy")
|
|
||||||
if mode == APIServerProxyModeNoAuth {
|
|
||||||
restConfig = rest.AnonymousClientConfig(restConfig)
|
restConfig = rest.AnonymousClientConfig(restConfig)
|
||||||
}
|
}
|
||||||
cfg, err := restConfig.TransportConfig()
|
cfg, err := restConfig.TransportConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
|
return nil, fmt.Errorf("could not get rest.TransportConfig(): %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
|
|
||||||
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
|
|
||||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
tr.TLSClientConfig, err = transport.TLSConfigFor(cfg)
|
tr.TLSClientConfig, err = transport.TLSConfigFor(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
startlog.Fatalf("could not get transport.TLSConfigFor(): %v", err)
|
return nil, fmt.Errorf("could not get transport.TLSConfigFor(): %w", err)
|
||||||
}
|
}
|
||||||
tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
|
tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
|
||||||
|
|
||||||
rt, err := transport.HTTPWrappersForConfig(cfg, tr)
|
rt, err := transport.HTTPWrappersForConfig(cfg, tr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
|
return nil, fmt.Errorf("could not get rest.TransportConfig(): %w", err)
|
||||||
}
|
}
|
||||||
go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy"), mode, restConfig.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// runAPIServerProxy runs an HTTP server that authenticates requests using the
|
u, err := url.Parse(restConfig.Host)
|
||||||
// Tailscale LocalAPI and then proxies them to the Kubernetes API.
|
|
||||||
// It listens on :443 and uses the Tailscale HTTPS certificate.
|
|
||||||
// s will be started if it is not already running.
|
|
||||||
// rt is used to proxy requests to the Kubernetes API.
|
|
||||||
//
|
|
||||||
// mode controls how the proxy behaves:
|
|
||||||
// - apiserverProxyModeDisabled: the proxy is not started.
|
|
||||||
// - apiserverProxyModeEnabled: the proxy is started and requests are impersonated using the
|
|
||||||
// caller's identity from the Tailscale LocalAPI.
|
|
||||||
// - apiserverProxyModeNoAuth: the proxy is started and requests are not impersonated and
|
|
||||||
// are passed through to the Kubernetes API.
|
|
||||||
//
|
|
||||||
// It never returns.
|
|
||||||
func runAPIServerProxy(ts *tsnet.Server, rt http.RoundTripper, log *zap.SugaredLogger, mode APIServerProxyMode, host string) {
|
|
||||||
if mode == APIServerProxyModeDisabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ln, err := ts.Listen("tcp", ":443")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("could not listen on :443: %v", err)
|
return nil, fmt.Errorf("runAPIServerProxy: failed to parse URL %w", err)
|
||||||
}
|
|
||||||
u, err := url.Parse(host)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("runAPIServerProxy: failed to parse URL %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lc, err := ts.LocalClient()
|
lc, err := ts.LocalClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("could not get local client: %v", err)
|
return nil, fmt.Errorf("could not get local client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ap := &apiserverProxy{
|
ap := &APIServerProxy{
|
||||||
log: log,
|
log: zlog,
|
||||||
lc: lc,
|
lc: lc,
|
||||||
mode: mode,
|
authMode: authMode,
|
||||||
upstreamURL: u,
|
upstreamURL: u,
|
||||||
ts: ts,
|
ts: ts,
|
||||||
}
|
}
|
||||||
@ -164,41 +90,69 @@ func runAPIServerProxy(ts *tsnet.Server, rt http.RoundTripper, log *zap.SugaredL
|
|||||||
Transport: rt,
|
Transport: rt,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts the HTTP server that authenticates requests using the
|
||||||
|
// Tailscale LocalAPI and then proxies them to the Kubernetes API.
|
||||||
|
// It listens on :443 and uses the Tailscale HTTPS certificate.
|
||||||
|
//
|
||||||
|
// It will only return once Close is called on the APIServerProxy.
|
||||||
|
func (ap *APIServerProxy) Run() error {
|
||||||
|
ln, err := ap.ts.Listen("tcp", ":443")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not listen on :443: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", ap.serveDefault)
|
mux.HandleFunc("/", ap.serveDefault)
|
||||||
mux.HandleFunc("POST /api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExecSPDY)
|
mux.HandleFunc("POST /api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExecSPDY)
|
||||||
mux.HandleFunc("GET /api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExecWS)
|
mux.HandleFunc("GET /api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExecWS)
|
||||||
|
|
||||||
hs := &http.Server{
|
ap.hs = &http.Server{
|
||||||
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
|
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
|
||||||
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
|
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
|
||||||
TLSConfig: &tls.Config{
|
TLSConfig: &tls.Config{
|
||||||
GetCertificate: lc.GetCertificate,
|
GetCertificate: ap.lc.GetCertificate,
|
||||||
NextProtos: []string{"http/1.1"},
|
NextProtos: []string{"http/1.1"},
|
||||||
},
|
},
|
||||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
}
|
}
|
||||||
log.Infof("API server proxy in %q mode is listening on %s", mode, ln.Addr())
|
ap.log.Infof("API server proxy is listening on %s with auth mode: %v", ln.Addr(), ap.authMode)
|
||||||
if err := hs.ServeTLS(ln, "", ""); err != nil {
|
if err := ap.hs.ServeTLS(ln, "", ""); err != nil && err != http.ErrServerClosed {
|
||||||
log.Fatalf("runAPIServerProxy: failed to serve %v", err)
|
return fmt.Errorf("failed to serve: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiserverProxy is an [net/http.Handler] that authenticates requests using the Tailscale
|
// Close stops the HTTP server.
|
||||||
|
func (ap *APIServerProxy) Close() error {
|
||||||
|
if ap.hs != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
return ap.hs.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIServerProxy is an [net/http.Handler] that authenticates requests using the Tailscale
|
||||||
// LocalAPI and then proxies them to the Kubernetes API.
|
// LocalAPI and then proxies them to the Kubernetes API.
|
||||||
type apiserverProxy struct {
|
type APIServerProxy struct {
|
||||||
log *zap.SugaredLogger
|
log *zap.SugaredLogger
|
||||||
lc *local.Client
|
lc *local.Client
|
||||||
rp *httputil.ReverseProxy
|
rp *httputil.ReverseProxy
|
||||||
|
|
||||||
mode APIServerProxyMode
|
authMode bool
|
||||||
ts *tsnet.Server
|
ts *tsnet.Server
|
||||||
|
hs *http.Server
|
||||||
upstreamURL *url.URL
|
upstreamURL *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveDefault is the default handler for Kubernetes API server requests.
|
// serveDefault is the default handler for Kubernetes API server requests.
|
||||||
func (ap *apiserverProxy) serveDefault(w http.ResponseWriter, r *http.Request) {
|
func (ap *APIServerProxy) serveDefault(w http.ResponseWriter, r *http.Request) {
|
||||||
who, err := ap.whoIs(r)
|
who, err := ap.whoIs(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ap.authError(w, err)
|
ap.authError(w, err)
|
||||||
@ -210,17 +164,17 @@ func (ap *apiserverProxy) serveDefault(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// serveExecSPDY serves 'kubectl exec' requests for sessions streamed over SPDY,
|
// serveExecSPDY serves 'kubectl exec' requests for sessions streamed over SPDY,
|
||||||
// optionally configuring the kubectl exec sessions to be recorded.
|
// optionally configuring the kubectl exec sessions to be recorded.
|
||||||
func (ap *apiserverProxy) serveExecSPDY(w http.ResponseWriter, r *http.Request) {
|
func (ap *APIServerProxy) serveExecSPDY(w http.ResponseWriter, r *http.Request) {
|
||||||
ap.execForProto(w, r, ksr.SPDYProtocol)
|
ap.execForProto(w, r, ksr.SPDYProtocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveExecWS serves 'kubectl exec' requests for sessions streamed over WebSocket,
|
// serveExecWS serves 'kubectl exec' requests for sessions streamed over WebSocket,
|
||||||
// optionally configuring the kubectl exec sessions to be recorded.
|
// optionally configuring the kubectl exec sessions to be recorded.
|
||||||
func (ap *apiserverProxy) serveExecWS(w http.ResponseWriter, r *http.Request) {
|
func (ap *APIServerProxy) serveExecWS(w http.ResponseWriter, r *http.Request) {
|
||||||
ap.execForProto(w, r, ksr.WSProtocol)
|
ap.execForProto(w, r, ksr.WSProtocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ap *apiserverProxy) execForProto(w http.ResponseWriter, r *http.Request, proto ksr.Protocol) {
|
func (ap *APIServerProxy) execForProto(w http.ResponseWriter, r *http.Request, proto ksr.Protocol) {
|
||||||
const (
|
const (
|
||||||
podNameKey = "pod"
|
podNameKey = "pod"
|
||||||
namespaceNameKey = "namespace"
|
namespaceNameKey = "namespace"
|
||||||
@ -282,10 +236,10 @@ func (ap *apiserverProxy) execForProto(w http.ResponseWriter, r *http.Request, p
|
|||||||
ap.rp.ServeHTTP(h, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
ap.rp.ServeHTTP(h, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *apiserverProxy) addImpersonationHeadersAsRequired(r *http.Request) {
|
func (ap *APIServerProxy) addImpersonationHeadersAsRequired(r *http.Request) {
|
||||||
r.URL.Scheme = h.upstreamURL.Scheme
|
r.URL.Scheme = ap.upstreamURL.Scheme
|
||||||
r.URL.Host = h.upstreamURL.Host
|
r.URL.Host = ap.upstreamURL.Host
|
||||||
if h.mode == APIServerProxyModeNoAuth {
|
if !ap.authMode {
|
||||||
// If we are not providing authentication, then we are just
|
// If we are not providing authentication, then we are just
|
||||||
// proxying to the Kubernetes API, so we don't need to do
|
// proxying to the Kubernetes API, so we don't need to do
|
||||||
// anything else.
|
// anything else.
|
||||||
@ -310,16 +264,16 @@ func (h *apiserverProxy) addImpersonationHeadersAsRequired(r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now add the impersonation headers that we want.
|
// Now add the impersonation headers that we want.
|
||||||
if err := addImpersonationHeaders(r, h.log); err != nil {
|
if err := addImpersonationHeaders(r, ap.log); err != nil {
|
||||||
log.Print("failed to add impersonation headers: ", err.Error())
|
ap.log.Errorf("failed to add impersonation headers: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ap *apiserverProxy) whoIs(r *http.Request) (*apitype.WhoIsResponse, error) {
|
func (ap *APIServerProxy) whoIs(r *http.Request) (*apitype.WhoIsResponse, error) {
|
||||||
return ap.lc.WhoIs(r.Context(), r.RemoteAddr)
|
return ap.lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ap *apiserverProxy) authError(w http.ResponseWriter, err error) {
|
func (ap *APIServerProxy) authError(w http.ResponseWriter, err error) {
|
||||||
ap.log.Errorf("failed to authenticate caller: %v", err)
|
ap.log.Errorf("failed to authenticate caller: %v", err)
|
||||||
http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
|
http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ package kubetypes
|
|||||||
const (
|
const (
|
||||||
// Hostinfo App values for the Tailscale Kubernetes Operator components.
|
// Hostinfo App values for the Tailscale Kubernetes Operator components.
|
||||||
AppOperator = "k8s-operator"
|
AppOperator = "k8s-operator"
|
||||||
|
AppProxy = "k8s-proxy"
|
||||||
AppAPIServerProxy = "k8s-operator-proxy"
|
AppAPIServerProxy = "k8s-operator-proxy"
|
||||||
AppIngressProxy = "k8s-operator-ingress-proxy"
|
AppIngressProxy = "k8s-operator-ingress-proxy"
|
||||||
AppIngressResource = "k8s-operator-ingress-resource"
|
AppIngressResource = "k8s-operator-ingress-resource"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user