|
|
@@ -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.CounterSessionRecordingsAttempted.Add(1) // at this point we know that users intended for this session to be recorded
|
|
|
- if !failOpen && len(addrs) == 0 {
|
|
|
+ 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: '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: addrs,
|
|
|
- FailOpen: failOpen,
|
|
|
+ Addrs: c.recorderAddresses,
|
|
|
+ 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{
|