2023-02-03 22:47:52 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
2023-08-24 22:02:42 +00:00
//go:build !plan9
2023-02-03 22:47:52 +00:00
package main
import (
2023-03-23 18:37:26 +00:00
"crypto/tls"
2023-02-03 22:47:52 +00:00
"fmt"
"log"
"net/http"
"net/http/httputil"
2024-07-08 20:18:55 +00:00
"net/netip"
2023-02-03 22:47:52 +00:00
"net/url"
"os"
"strings"
2024-07-08 20:18:55 +00:00
"github.com/pkg/errors"
2023-08-23 15:20:14 +00:00
"go.uber.org/zap"
"k8s.io/client-go/rest"
"k8s.io/client-go/transport"
2023-02-03 22:47:52 +00:00
"tailscale.com/client/tailscale"
"tailscale.com/client/tailscale/apitype"
2024-06-10 15:36:22 +00:00
tskube "tailscale.com/kube"
2024-07-08 20:18:55 +00:00
"tailscale.com/ssh/tailssh"
2023-08-16 20:20:55 +00:00
"tailscale.com/tailcfg"
2023-03-13 19:06:24 +00:00
"tailscale.com/tsnet"
2023-08-30 16:49:11 +00:00
"tailscale.com/util/clientmetric"
2024-01-16 21:56:23 +00:00
"tailscale.com/util/ctxkey"
2023-08-16 20:20:55 +00:00
"tailscale.com/util/set"
2023-02-03 22:47:52 +00:00
)
2024-01-16 21:56:23 +00:00
var whoIsKey = ctxkey . New ( "" , ( * apitype . WhoIsResponse ) ( nil ) )
2023-08-16 20:20:55 +00:00
2023-08-30 16:49:11 +00:00
var counterNumRequestsProxied = clientmetric . NewCounter ( "k8s_auth_proxy_requests_proxied" )
2023-09-14 15:53:21 +00:00
type apiServerProxyMode int
2024-07-08 20:18:55 +00:00
func ( a apiServerProxyMode ) String ( ) string {
switch a {
case apiserverProxyModeDisabled :
return "disabled"
case apiserverProxyModeEnabled :
return "auth"
case apiserverProxyModeNoAuth :
return "noauth"
default :
return "unknown"
}
}
2023-09-14 15:53:21 +00:00
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.
2023-11-02 14:36:20 +00:00
func maybeLaunchAPIServerProxy ( zlog * zap . SugaredLogger , restConfig * rest . Config , s * tsnet . Server , mode apiServerProxyMode ) {
2023-09-14 15:53:21 +00:00
if mode == apiserverProxyModeDisabled {
return
}
startlog := zlog . Named ( "launchAPIProxy" )
2023-11-01 19:24:44 +00:00
if mode == apiserverProxyModeNoAuth {
restConfig = rest . AnonymousClientConfig ( restConfig )
}
2023-08-23 15:20:14 +00:00
cfg , err := restConfig . TransportConfig ( )
if err != nil {
startlog . Fatalf ( "could not get rest.TransportConfig(): %v" , 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 . TLSClientConfig , err = transport . TLSConfigFor ( cfg )
if err != nil {
startlog . Fatalf ( "could not get transport.TLSConfigFor(): %v" , err )
}
tr . TLSNextProto = make ( map [ string ] func ( authority string , c * tls . Conn ) http . RoundTripper )
rt , err := transport . HTTPWrappersForConfig ( cfg , tr )
if err != nil {
startlog . Fatalf ( "could not get rest.TransportConfig(): %v" , err )
}
2024-07-08 20:18:55 +00:00
go runAPIServerProxy ( s , rt , zlog . Named ( "apiserver-proxy" ) , mode , restConfig . Host )
2023-02-03 22:47:52 +00:00
}
2023-09-14 15:53:21 +00:00
// runAPIServerProxy runs an HTTP server that authenticates requests using the
2023-03-13 19:06:24 +00:00
// 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.
//
2023-09-14 15:53:21 +00:00
// 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.
//
2023-03-13 19:06:24 +00:00
// It never returns.
2024-07-08 20:18:55 +00:00
func runAPIServerProxy ( ts * tsnet . Server , rt http . RoundTripper , log * zap . SugaredLogger , mode apiServerProxyMode , host string ) {
2023-09-14 15:53:21 +00:00
if mode == apiserverProxyModeDisabled {
return
}
2024-07-08 20:18:55 +00:00
ln , err := ts . Listen ( "tcp" , ":443" )
2023-03-13 19:06:24 +00:00
if err != nil {
log . Fatalf ( "could not listen on :443: %v" , err )
}
2024-07-08 20:18:55 +00:00
u , err := url . Parse ( host )
2023-02-03 22:47:52 +00:00
if err != nil {
2023-09-14 15:53:21 +00:00
log . Fatalf ( "runAPIServerProxy: failed to parse URL %v" , err )
2023-02-03 22:47:52 +00:00
}
2023-03-13 19:06:24 +00:00
2024-07-08 20:18:55 +00:00
lc , err := ts . LocalClient ( )
2023-03-13 19:06:24 +00:00
if err != nil {
log . Fatalf ( "could not get local client: %v" , err )
}
2024-07-08 20:18:55 +00:00
2023-09-14 15:53:21 +00:00
ap := & apiserverProxy {
2024-07-08 20:18:55 +00:00
log : log ,
lc : lc ,
mode : mode ,
upstreamURL : u ,
ts : ts ,
}
ap . rp = & httputil . ReverseProxy {
Rewrite : func ( pr * httputil . ProxyRequest ) {
ap . addImpersonationHeadersAsRequired ( pr . Out )
2023-02-03 22:47:52 +00:00
} ,
2024-07-08 20:18:55 +00:00
Transport : rt ,
2023-02-03 22:47:52 +00:00
}
2024-07-08 20:18:55 +00:00
mux := http . NewServeMux ( )
mux . HandleFunc ( "/" , ap . serveDefault )
mux . HandleFunc ( "/api/v1/namespaces/{namespace}/pods/{pod}/exec" , ap . serveExec )
2023-03-23 18:37:26 +00:00
hs := & http . Server {
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
TLSConfig : & tls . Config {
GetCertificate : lc . GetCertificate ,
NextProtos : [ ] string { "http/1.1" } ,
} ,
TLSNextProto : make ( map [ string ] func ( * http . Server , * tls . Conn , http . Handler ) ) ,
2024-07-08 20:18:55 +00:00
Handler : mux ,
2023-03-23 18:37:26 +00:00
}
2024-07-08 20:18:55 +00:00
log . Infof ( "API server proxy in %q mode is listening on %s" , mode , ln . Addr ( ) )
2023-03-23 18:37:26 +00:00
if err := hs . ServeTLS ( ln , "" , "" ) ; err != nil {
2023-09-14 15:53:21 +00:00
log . Fatalf ( "runAPIServerProxy: failed to serve %v" , err )
2023-02-03 22:47:52 +00:00
}
}
2023-08-16 20:20:55 +00:00
2024-07-08 20:18:55 +00:00
// apiserverProxy is an [net/http.Handler] that authenticates requests using the Tailscale
// LocalAPI and then proxies them to the Kubernetes API.
type apiserverProxy struct {
log * zap . SugaredLogger
lc * tailscale . LocalClient
rp * httputil . ReverseProxy
mode apiServerProxyMode
ts * tsnet . Server
upstreamURL * url . URL
}
// serveDefault is the default handler for Kubernetes API server requests.
func ( ap * apiserverProxy ) serveDefault ( w http . ResponseWriter , r * http . Request ) {
who , err := ap . whoIs ( r )
if err != nil {
ap . authError ( w , err )
return
}
counterNumRequestsProxied . Add ( 1 )
ap . rp . ServeHTTP ( w , r . WithContext ( whoIsKey . WithValue ( r . Context ( ) , who ) ) )
}
// serveExec serves 'kubectl exec' requests, optionally configuring the kubectl
// exec sessions to be recorded.
func ( ap * apiserverProxy ) serveExec ( w http . ResponseWriter , r * http . Request ) {
who , err := ap . whoIs ( r )
if err != nil {
ap . authError ( w , err )
return
}
counterNumRequestsProxied . Add ( 1 )
failOpen , addrs , err := determineRecorderConfig ( who )
if err != nil {
ap . log . Errorf ( "error trying to determine whether the 'kubectl exec' session needs to be recorded: %v" , err )
return
}
if failOpen && len ( addrs ) == 0 { // will not record
ap . rp . ServeHTTP ( w , r . WithContext ( whoIsKey . WithValue ( r . Context ( ) , who ) ) )
return
}
if ! failOpen && len ( addrs ) == 0 {
msg := "forbidden: 'kubectl exec' session must be recorded, but no recorders are available."
ap . log . Error ( msg )
http . Error ( w , msg , http . StatusForbidden )
return
}
if r . Method != "POST" || r . Header . Get ( "Upgrade" ) != "SPDY/3.1" {
msg := "'kubectl exec' session recording is configured, but the request is not over SPDY. Session recording is currently only supported for SPDY based clients"
if failOpen {
msg = msg + "; failure mode is 'fail open'; continuing session without recording."
ap . log . Warn ( msg )
ap . rp . ServeHTTP ( w , r . WithContext ( whoIsKey . WithValue ( r . Context ( ) , who ) ) )
return
}
ap . log . Error ( msg )
msg += "; failure mode is 'fail closed'; closing connection."
http . Error ( w , msg , http . StatusForbidden )
return
}
spdyH := & spdyHijacker {
ts : ap . ts ,
req : r ,
who : who ,
ResponseWriter : w ,
log : ap . log ,
pod : r . PathValue ( "pod" ) ,
ns : r . PathValue ( "namespace" ) ,
addrs : addrs ,
failOpen : failOpen ,
connectToRecorder : tailssh . ConnectToRecorder ,
}
ap . rp . ServeHTTP ( spdyH , r . WithContext ( whoIsKey . WithValue ( r . Context ( ) , who ) ) )
}
func ( h * apiserverProxy ) addImpersonationHeadersAsRequired ( r * http . Request ) {
r . URL . Scheme = h . upstreamURL . Scheme
r . URL . Host = h . upstreamURL . Host
if h . mode == apiserverProxyModeNoAuth {
// If we are not providing authentication, then we are just
// proxying to the Kubernetes API, so we don't need to do
// anything else.
return
}
// We want to proxy to the Kubernetes API, but we want to use
// the caller's identity to do so. We do this by impersonating
// the caller using the Kubernetes User Impersonation feature:
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
// Out of paranoia, remove all authentication headers that might
// have been set by the client.
r . Header . Del ( "Authorization" )
r . Header . Del ( "Impersonate-Group" )
r . Header . Del ( "Impersonate-User" )
r . Header . Del ( "Impersonate-Uid" )
for k := range r . Header {
if strings . HasPrefix ( k , "Impersonate-Extra-" ) {
r . Header . Del ( k )
}
}
// Now add the impersonation headers that we want.
if err := addImpersonationHeaders ( r , h . log ) ; err != nil {
log . Printf ( "failed to add impersonation headers: " + err . Error ( ) )
}
}
func ( ap * apiserverProxy ) whoIs ( r * http . Request ) ( * apitype . WhoIsResponse , error ) {
return ap . lc . WhoIs ( r . Context ( ) , r . RemoteAddr )
}
func ( ap * apiserverProxy ) authError ( w http . ResponseWriter , err error ) {
ap . log . Errorf ( "failed to authenticate caller: %v" , err )
http . Error ( w , "failed to authenticate caller" , http . StatusInternalServerError )
}
2023-12-11 19:51:20 +00:00
const (
2024-06-04 17:31:37 +00:00
// oldCapabilityName is a legacy form of
// tailfcg.PeerCapabilityKubernetes capability. The only capability rule
// that is respected for this form is group impersonation - for
// backwards compatibility reasons.
2024-06-10 15:36:22 +00:00
// TODO (irbekrm): determine if anyone uses this and remove if possible.
2024-06-04 17:31:37 +00:00
oldCapabilityName = "https://" + tailcfg . PeerCapabilityKubernetes
2023-12-11 19:51:20 +00:00
)
2023-08-16 20:20:55 +00:00
// addImpersonationHeaders adds the appropriate headers to r to impersonate the
// caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed
2023-09-14 15:53:21 +00:00
// in the context by the apiserverProxy.
2023-11-20 15:41:18 +00:00
func addImpersonationHeaders ( r * http . Request , log * zap . SugaredLogger ) error {
log = log . With ( "remote" , r . RemoteAddr )
2024-01-16 21:56:23 +00:00
who := whoIsKey . Value ( r . Context ( ) )
2024-06-10 15:36:22 +00:00
rules , err := tailcfg . UnmarshalCapJSON [ tskube . KubernetesCapRule ] ( who . CapMap , tailcfg . PeerCapabilityKubernetes )
2023-12-11 19:51:20 +00:00
if len ( rules ) == 0 && err == nil {
// Try the old capability name for backwards compatibility.
2024-06-10 15:36:22 +00:00
rules , err = tailcfg . UnmarshalCapJSON [ tskube . KubernetesCapRule ] ( who . CapMap , oldCapabilityName )
2023-12-11 19:51:20 +00:00
}
2023-08-16 20:20:55 +00:00
if err != nil {
return fmt . Errorf ( "failed to unmarshal capability: %v" , err )
}
var groupsAdded set . Slice [ string ]
for _ , rule := range rules {
if rule . Impersonate == nil {
continue
}
for _ , group := range rule . Impersonate . Groups {
if groupsAdded . Contains ( group ) {
continue
}
r . Header . Add ( "Impersonate-Group" , group )
groupsAdded . Add ( group )
2023-11-20 15:41:18 +00:00
log . Debugf ( "adding group impersonation header for user group %s" , group )
2023-08-16 20:20:55 +00:00
}
}
if ! who . Node . IsTagged ( ) {
r . Header . Set ( "Impersonate-User" , who . UserProfile . LoginName )
2023-11-20 15:41:18 +00:00
log . Debugf ( "adding user impersonation header for user %s" , who . UserProfile . LoginName )
2023-08-16 20:20:55 +00:00
return nil
}
// "Impersonate-Group" requires "Impersonate-User" to be set, so we set it
// to the node FQDN for tagged nodes.
2023-11-20 15:41:18 +00:00
nodeName := strings . TrimSuffix ( who . Node . Name , "." )
r . Header . Set ( "Impersonate-User" , nodeName )
log . Debugf ( "adding user impersonation header for node name %s" , nodeName )
2023-08-16 20:20:55 +00:00
// For legacy behavior (before caps), set the groups to the nodes tags.
if groupsAdded . Slice ( ) . Len ( ) == 0 {
for _ , tag := range who . Node . Tags {
r . Header . Add ( "Impersonate-Group" , tag )
2023-11-20 15:41:18 +00:00
log . Debugf ( "adding group impersonation header for node tag %s" , tag )
2023-08-16 20:20:55 +00:00
}
}
return nil
}
2024-07-08 20:18:55 +00:00
// determineRecorderConfig determines recorder config from requester's peer
// capabilities. Determines whether a 'kubectl exec' session from this requester
// needs to be recorded and what recorders the recording should be sent to.
func determineRecorderConfig ( who * apitype . WhoIsResponse ) ( failOpen bool , recorderAddresses [ ] netip . AddrPort , _ error ) {
if who == nil {
return false , nil , errors . New ( "[unexpected] cannot determine caller" )
}
failOpen = true
rules , err := tailcfg . UnmarshalCapJSON [ tskube . KubernetesCapRule ] ( who . CapMap , tailcfg . PeerCapabilityKubernetes )
if err != nil {
return failOpen , nil , fmt . Errorf ( "failed to unmarshal Kubernetes capability: %w" , err )
}
if len ( rules ) == 0 {
return failOpen , nil , nil
}
for _ , rule := range rules {
if len ( rule . RecorderAddrs ) != 0 {
// TODO (irbekrm): here or later determine if the
// recorders behind those addrs are online - else we
// spend 30s trying to reach a recorder whose tailscale
// status is offline.
recorderAddresses = append ( recorderAddresses , rule . RecorderAddrs ... )
}
if rule . EnforceRecorder {
failOpen = false
}
}
return failOpen , recorderAddresses , nil
}