@ -46,6 +46,10 @@ var (
whoIsKey = ctxkey . New ( "" , ( * apitype . WhoIsResponse ) ( nil ) )
)
const (
eventsEnabledVar = "TS_EXPERIMENTAL_KUBE_API_EVENTS"
)
// NewAPIServerProxy creates a new APIServerProxy that's ready to start once Run
// is called. No network traffic will flow until Run is called.
//
@ -97,7 +101,7 @@ func NewAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, ts *tsn
upstreamURL : u ,
ts : ts ,
sendEventFunc : sessionrecording . SendEvent ,
eventsEnabled : envknob . Bool ( "TS_EXPERIMENTAL_KUBE_API_EVENTS" ) ,
eventsEnabled : envknob . Bool ( eventsEnabledVar ) ,
}
ap . rp = & httputil . ReverseProxy {
Rewrite : func ( pr * httputil . ProxyRequest ) {
@ -128,6 +132,10 @@ func (ap *APIServerProxy) Run(ctx context.Context) error {
TLSNextProto : make ( map [ string ] func ( * http . Server , * tls . Conn , http . Handler ) ) ,
}
if ap . eventsEnabled {
ap . log . Warnf ( "DEPRECATED: %q environment variable is deprecated, and will be removed in v1.96. See documentation for more detail." , eventsEnabledVar )
}
mode := "noauth"
if ap . authMode {
mode = "auth"
@ -196,6 +204,7 @@ type APIServerProxy struct {
sendEventFunc func ( ap netip . AddrPort , event io . Reader , dial netx . DialFunc ) error
// Flag used to enable sending API requests as events to tsrecorder.
// Deprecated: events are now set via ACLs (see https://tailscale.com/kb/1246/tailscale-ssh-session-recording#turn-on-session-recording-in-your-tailnet-policy-file)
eventsEnabled bool
}
@ -207,13 +216,34 @@ func (ap *APIServerProxy) serveDefault(w http.ResponseWriter, r *http.Request) {
return
}
if err = ap . recordRequestAsEvent ( r , who ) ; err != nil {
msg := fmt . Sprintf ( "error recording Kubernetes API request: %v" , err )
ap . log . Errorf ( msg )
http . Error ( w , msg , http . StatusBadGateway )
c , err := determineRecorderConfig ( who )
if err != nil {
ap . log . Errorf ( "error trying to determine whether the kubernetes api request %q needs to be recorded: %v" , r . URL . String ( ) , err )
return
}
if c . failOpen && len ( c . recorderAddresses ) == 0 { // will not record
ap . rp . ServeHTTP ( w , r . WithContext ( whoIsKey . WithValue ( r . Context ( ) , who ) ) )
return
}
ksr . CounterKubernetesAPIRequestEventsAttempted . Add ( 1 ) // at this point we know that users intended for this request to be recorded
if ! c . failOpen && len ( c . recorderAddresses ) == 0 {
msg := fmt . Sprintf ( "forbidden: api request %q must be recorded, but no recorders are available." , r . URL . String ( ) )
ap . log . Error ( msg )
http . Error ( w , msg , http . StatusForbidden )
return
}
// NOTE: (ChaosInTheCRD) ap.eventsEnabled deprecated, remove in v1.96
if c . enableEvents || ap . eventsEnabled {
if err = ap . recordRequestAsEvent ( r , who , c . recorderAddresses , c . failOpen ) ; err != nil {
msg := fmt . Sprintf ( "error recording Kubernetes API request: %v" , err )
ap . log . Errorf ( msg )
http . Error ( w , msg , http . StatusBadGateway )
return
}
}
counterNumRequestsProxied . Add ( 1 )
ap . rp . ServeHTTP ( w , r . WithContext ( whoIsKey . WithValue ( r . Context ( ) , who ) ) )
@ -256,35 +286,45 @@ func (ap *APIServerProxy) sessionForProto(w http.ResponseWriter, r *http.Request
return
}
if err = ap . recordRequestAsEvent ( r , who ) ; err != nil {
msg := fmt . Sprintf ( "error recording Kubernetes API request: %v" , err )
ap . log . Errorf ( msg )
http . Error ( w , msg , http . StatusBadGateway )
return
}
counterNumRequestsProxied . Add ( 1 )
failOpen, addrs , err := determineRecorderConfig ( who )
c , err := determineRecorderConfig ( who )
if err != nil {
ap . log . Errorf ( "error trying to determine whether the 'kubectl %s' session needs to be recorded: %v" , sessionType , err )
return
}
if failOpen && len ( addrs ) == 0 { // will not record
if c . failOpen && len ( c . recorderAddresses ) == 0 { // will not record
ap . rp . ServeHTTP ( w , r . WithContext ( whoIsKey . WithValue ( r . Context ( ) , who ) ) )
return
}
ksr . Counter SessionRecording sAttempted. Add ( 1 ) // at this point we know that users intended for this session to be recorded
if ! failOpen && len ( addr s) == 0 {
ksr . Counter KubernetesAPIRequestEvent sAttempted. Add ( 1 ) // at this point we know that users intended for this request to be recorded
if ! c. failOpen && len ( c. recorderAddresse s) == 0 {
msg := fmt . Sprintf ( "forbidden: 'kubectl %s' session must be recorded, but no recorders are available." , sessionType )
ap . log . Error ( msg )
http . Error ( w , msg , http . StatusForbidden )
return
}
// NOTE: (ChaosInTheCRD) ap.eventsEnabled deprecated, remove in v1.96
if c . enableEvents || ap . eventsEnabled {
if err = ap . recordRequestAsEvent ( r , who , c . recorderAddresses , c . failOpen ) ; err != nil {
msg := fmt . Sprintf ( "error recording Kubernetes API request: %v" , err )
ap . log . Errorf ( msg )
http . Error ( w , msg , http . StatusBadGateway )
return
}
}
if ! c . enableRecordings {
ap . rp . ServeHTTP ( w , r . WithContext ( whoIsKey . WithValue ( r . Context ( ) , who ) ) )
return
}
ksr . CounterSessionRecordingsAttempted . Add ( 1 ) // at this point we know that users intended for this session to be recorded
wantsHeader := upgradeHeaderForProto [ proto ]
if h := r . Header . Get ( upgradeHeaderKey ) ; h != wantsHeader {
msg := fmt . Sprintf ( "[unexpected] unable to verify that streaming protocol is %s, wants Upgrade header %q, got: %q" , proto , wantsHeader , h )
if failOpen {
if c. 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 ) ) )
@ -303,8 +343,8 @@ func (ap *APIServerProxy) sessionForProto(w http.ResponseWriter, r *http.Request
SessionType : sessionType ,
TS : ap . ts ,
Who : who ,
Addrs : addr s,
FailOpen : failOpen,
Addrs : c. recorderAddresse s,
FailOpen : c. failOpen,
Pod : r . PathValue ( podNameKey ) ,
Namespace : r . PathValue ( namespaceNameKey ) ,
Log : ap . log ,
@ -314,21 +354,9 @@ func (ap *APIServerProxy) sessionForProto(w http.ResponseWriter, r *http.Request
ap . rp . ServeHTTP ( h , r . WithContext ( whoIsKey . WithValue ( r . Context ( ) , who ) ) )
}
func ( ap * APIServerProxy ) recordRequestAsEvent ( req * http . Request , who * apitype . WhoIsResponse ) error {
if ! ap . eventsEnabled {
return nil
}
failOpen , addrs , err := determineRecorderConfig ( who )
if err != nil {
return fmt . Errorf ( "error trying to determine whether the kubernetes api request needs to be recorded: %w" , err )
}
func ( ap * APIServerProxy ) recordRequestAsEvent ( req * http . Request , who * apitype . WhoIsResponse , addrs [ ] netip . AddrPort , failOpen bool ) error {
if len ( addrs ) == 0 {
if failOpen {
return nil
} else {
return fmt . Errorf ( "forbidden: kubernetes api request must be recorded, but no recorders are available" )
}
return fmt . Errorf ( "no recorder addresses specified" )
}
factory := & request . RequestInfoFactory {
@ -537,20 +565,30 @@ func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error {
return nil
}
type recorderConfig struct {
failOpen bool
enableEvents bool
enableRecordings bool
recorderAddresses [ ] netip . AddrPort
}
// 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 ) {
func determineRecorderConfig ( who * apitype . WhoIsResponse ) ( c recorderConfig , _ error ) {
if who == nil {
return false , nil , errors . New ( "[unexpected] cannot determine caller" )
return c , errors . New ( "[unexpected] cannot determine caller" )
}
failOpen = true
c . failOpen = true
c . enableEvents = false
c . enableRecordings = true
rules , err := tailcfg . UnmarshalCapJSON [ kubetypes . KubernetesCapRule ] ( who . CapMap , tailcfg . PeerCapabilityKubernetes )
if err != nil {
return failOpen , nil , fmt . Errorf ( "failed to unmarshal Kubernetes capability: %w" , err )
return c , fmt . Errorf ( "failed to unmarshal Kubernetes capability: %w" , err )
}
if len ( rules ) == 0 {
return failOpen, nil , nil
return c , nil
}
for _ , rule := range rules {
@ -559,13 +597,19 @@ func determineRecorderConfig(who *apitype.WhoIsResponse) (failOpen bool, recorde
// 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 ... )
c. recorderAddresses = append ( c . recorderAddresses , rule . RecorderAddrs ... )
}
if rule . EnforceRecorder {
failOpen = false
c . failOpen = false
}
if rule . EnableEvents {
c . enableEvents = true
}
if rule . EnableSessionRecordings {
c . enableRecordings = true
}
}
return failOpen , recorderAddresses , nil
return c, nil
}
var upgradeHeaderForProto = map [ ksr . Protocol ] string {